// 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/IChainAdapter.sol"; import "../../UniversalCCIPBridge.sol"; /** * @title XDCAdapter * @notice Bridge adapter for XDC Network (EVM-compatible with xdc address prefix) * @dev XDC uses xdc prefix instead of 0x for addresses */ contract XDCAdapter is IChainAdapter, AccessControl, ReentrancyGuard { using SafeERC20 for IERC20; bytes32 public constant BRIDGE_OPERATOR_ROLE = keccak256("BRIDGE_OPERATOR_ROLE"); uint256 public constant XDC_MAINNET = 50; uint256 public constant XDC_APOTHEM_TESTNET = 51; UniversalCCIPBridge public universalBridge; bool public isActive; mapping(bytes32 => BridgeRequest) public bridgeRequests; mapping(address => uint256) public nonces; event XDCBridgeInitiated( bytes32 indexed requestId, address indexed sender, address indexed token, uint256 amount, string xdcDestination ); event XDCBridgeConfirmed( bytes32 indexed requestId, bytes32 indexed xdcTxHash ); constructor(address admin, address _bridge) { _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(BRIDGE_OPERATOR_ROLE, admin); universalBridge = UniversalCCIPBridge(payable(_bridge)); isActive = true; } function getChainType() external pure override returns (string memory) { return "XDC"; } function getChainIdentifier() external pure override returns (uint256 chainId, string memory identifier) { return (XDC_MAINNET, "XDC-Mainnet"); } function validateDestination(bytes calldata destination) external pure override returns (bool) { string memory addr = string(destination); bytes memory addrBytes = bytes(addr); if (addrBytes.length != 43) return false; if (addrBytes[0] != 'x' || addrBytes[1] != 'd' || addrBytes[2] != 'c') return false; for (uint256 i = 3; i < 43; i++) { bytes1 char = addrBytes[i]; if (!((char >= 0x30 && char <= 0x39) || (char >= 0x61 && char <= 0x66))) { return false; } } return true; } function convertXdcToEth(string memory xdcAddr) public pure returns (address) { bytes memory xdcBytes = bytes(xdcAddr); require(xdcBytes.length == 43, "Invalid XDC address length"); require(xdcBytes[0] == 'x' && xdcBytes[1] == 'd' && xdcBytes[2] == 'c', "Invalid XDC prefix"); // Parse 40 hex chars (20 bytes) to address; do not use bytes32(hexBytes) which truncates 42 bytes uint160 result = 0; for (uint256 i = 0; i < 40; i++) { result = result * 16 + _hexCharToNibble(xdcBytes[i + 3]); } return address(result); } function _hexCharToNibble(bytes1 c) internal pure returns (uint8) { if (c >= 0x30 && c <= 0x39) return uint8(c) - 0x30; // '0'-'9' if (c >= 0x61 && c <= 0x66) return uint8(c) - 0x61 + 10; // 'a'-'f' if (c >= 0x41 && c <= 0x46) return uint8(c) - 0x41 + 10; // 'A'-'F' revert("Invalid hex character"); } function convertEthToXdc(address ethAddr) public pure returns (string memory) { bytes20 addr = bytes20(ethAddr); bytes memory hexString = new bytes(43); hexString[0] = 'x'; hexString[1] = 'd'; hexString[2] = 'c'; for (uint256 i = 0; i < 20; i++) { uint8 byteValue = uint8(addr[i]); uint8 high = byteValue >> 4; uint8 low = byteValue & 0x0f; hexString[3 + i * 2] = bytes1(high < 10 ? 48 + high : 87 + high); hexString[4 + i * 2] = bytes1(low < 10 ? 48 + low : 87 + low); } return string(hexString); } function bridge( address token, uint256 amount, bytes calldata destination, bytes calldata recipient ) external payable override nonReentrant returns (bytes32 requestId) { require(isActive, "Adapter inactive"); require(amount > 0, "Zero amount"); string memory xdcDestination = string(destination); require(this.validateDestination(destination), "Invalid XDC address"); address evmRecipient = convertXdcToEth(xdcDestination); requestId = keccak256(abi.encodePacked( msg.sender, token, amount, xdcDestination, nonces[msg.sender]++, block.timestamp )); if (token == address(0)) { require(msg.value >= amount, "Insufficient ETH"); } else { IERC20(token).safeTransferFrom(msg.sender, address(this), amount); } bridgeRequests[requestId] = BridgeRequest({ sender: msg.sender, token: token, amount: amount, destinationData: destination, requestId: requestId, status: BridgeStatus.Locked, createdAt: block.timestamp, completedAt: 0 }); UniversalCCIPBridge.BridgeOperation memory op = UniversalCCIPBridge.BridgeOperation({ token: token, amount: amount, destinationChain: uint64(XDC_MAINNET), recipient: evmRecipient, assetType: bytes32(0), usePMM: false, useVault: false, complianceProof: "", vaultInstructions: "" }); bytes32 messageId = universalBridge.bridge{value: msg.value}(op); emit XDCBridgeInitiated(requestId, msg.sender, token, amount, xdcDestination); return requestId; } function getBridgeStatus(bytes32 requestId) external view override returns (BridgeRequest memory) { return bridgeRequests[requestId]; } function cancelBridge(bytes32 requestId) external override returns (bool) { BridgeRequest storage request = bridgeRequests[requestId]; require(request.status == BridgeStatus.Pending || request.status == BridgeStatus.Locked, "Cannot cancel"); require(msg.sender == request.sender, "Not request sender"); if (request.token == address(0)) { payable(request.sender).transfer(request.amount); } else { IERC20(request.token).safeTransfer(request.sender, request.amount); } request.status = BridgeStatus.Cancelled; return true; } function estimateFee( address token, uint256 amount, bytes calldata destination ) external view override returns (uint256 fee) { return 1000000000000000; } function setIsActive(bool _isActive) external onlyRole(DEFAULT_ADMIN_ROLE) { isActive = _isActive; } function confirmBridge(bytes32 requestId, bytes32 xdcTxHash) external onlyRole(BRIDGE_OPERATOR_ROLE) { BridgeRequest storage request = bridgeRequests[requestId]; require(request.status == BridgeStatus.Locked, "Invalid status"); request.status = BridgeStatus.Confirmed; request.completedAt = block.timestamp; emit XDCBridgeConfirmed(requestId, xdcTxHash); } }