// 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"; /** * @title TronAdapter * @notice Bridge adapter for Tron using an off-chain relay/oracle path * @dev Keeps fulfillment ids to make relay retries idempotent. */ contract TronAdapter is IChainAdapter, AccessControl, ReentrancyGuard { using SafeERC20 for IERC20; bytes32 public constant BRIDGE_OPERATOR_ROLE = keccak256("BRIDGE_OPERATOR_ROLE"); bytes32 public constant ORACLE_ROLE = keccak256("ORACLE_ROLE"); bool public isActive; uint256 public minFinalityBlocks; mapping(bytes32 => BridgeRequest) public bridgeRequests; mapping(bytes32 => string) public txHashes; mapping(bytes32 => uint256) public finalizedBlocks; mapping(bytes32 => bytes32) public fulfillmentIdsByRequest; mapping(bytes32 => bool) public usedFulfillmentIds; mapping(address => uint256) public nonces; event TronBridgeInitiated( bytes32 indexed requestId, address indexed sender, address indexed token, uint256 amount, string destination ); event TronBridgeConfirmed( bytes32 indexed requestId, string txHash, uint256 finalizedBlock, bytes32 indexed fulfillmentId ); event TronBridgeMarkedFailed(bytes32 indexed requestId, string reason); constructor(address admin) { _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(BRIDGE_OPERATOR_ROLE, admin); _grantRole(ORACLE_ROLE, admin); isActive = true; minFinalityBlocks = 20; } function getChainType() external pure override returns (string memory) { return "Tron"; } function getChainIdentifier() external pure override returns (uint256 chainId, string memory identifier) { return (0, "Tron-Mainnet"); } function validateDestination(bytes calldata destination) external pure override returns (bool) { uint256 length = destination.length; return length >= 34 && length <= 36; } 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"); require(this.validateDestination(destination), "Invalid Tron destination"); requestId = keccak256( abi.encodePacked( msg.sender, token, amount, destination, recipient, 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 }); emit TronBridgeInitiated(requestId, msg.sender, token, amount, string(destination)); return requestId; } function confirmTransaction( bytes32 requestId, string calldata txHash, uint256 finalizedBlock, bytes32 fulfillmentId ) external onlyRole(ORACLE_ROLE) { BridgeRequest storage request = bridgeRequests[requestId]; require(request.status == BridgeStatus.Locked, "Invalid status"); require(bytes(txHash).length > 0, "Missing Tron tx hash"); require(finalizedBlock >= minFinalityBlocks, "Insufficient finality"); require(fulfillmentId != bytes32(0), "Missing fulfillment id"); require(!usedFulfillmentIds[fulfillmentId], "Fulfillment already used"); usedFulfillmentIds[fulfillmentId] = true; fulfillmentIdsByRequest[requestId] = fulfillmentId; txHashes[requestId] = txHash; finalizedBlocks[requestId] = finalizedBlock; request.status = BridgeStatus.Confirmed; request.completedAt = block.timestamp; emit TronBridgeConfirmed(requestId, txHash, finalizedBlock, fulfillmentId); } function markFailed(bytes32 requestId, string calldata reason) external onlyRole(ORACLE_ROLE) { BridgeRequest storage request = bridgeRequests[requestId]; require(request.status == BridgeStatus.Locked, "Invalid status"); request.status = BridgeStatus.Failed; request.completedAt = block.timestamp; emit TronBridgeMarkedFailed(requestId, reason); } function getBridgeStatus(bytes32 requestId) external view override returns (BridgeRequest memory) { return bridgeRequests[requestId]; } function cancelBridge(bytes32 requestId) external override nonReentrant 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; request.completedAt = block.timestamp; return true; } function estimateFee( address, uint256, bytes calldata ) external pure override returns (uint256 fee) { return 1000000000000000; } function setIsActive(bool _isActive) external onlyRole(DEFAULT_ADMIN_ROLE) { isActive = _isActive; } function setMinFinalityBlocks(uint256 blocks_) external onlyRole(DEFAULT_ADMIN_ROLE) { require(blocks_ > 0, "Zero finality"); minFinalityBlocks = blocks_; } }