214 lines
7.4 KiB
Solidity
214 lines
7.4 KiB
Solidity
// 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);
|
|
}
|
|
}
|