feat: expand non-evm relay and route planning support

This commit is contained in:
defiQUG
2026-04-18 12:05:34 -07:00
parent da78073104
commit 843cdbf71c
113 changed files with 8542 additions and 222 deletions

View File

@@ -0,0 +1,209 @@
// 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 SolanaAdapter
* @notice Bridge adapter for Solana mainnet-beta using an off-chain relay/oracle path
* @dev Tracks idempotent fulfillment ids so the relay can resume safely after restarts.
*/
contract SolanaAdapter 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 minFinalitySlots;
mapping(bytes32 => BridgeRequest) public bridgeRequests;
mapping(bytes32 => string) public solanaTxSignatures;
mapping(bytes32 => uint256) public confirmedSlots;
mapping(bytes32 => bytes32) public fulfillmentIdsByRequest;
mapping(bytes32 => bool) public usedFulfillmentIds;
mapping(address => uint256) public nonces;
event SolanaBridgeInitiated(
bytes32 indexed requestId,
address indexed sender,
address indexed token,
uint256 amount,
string destination,
bytes recipient
);
event SolanaBridgeConfirmed(
bytes32 indexed requestId,
string txSignature,
uint256 finalizedSlot,
bytes32 indexed fulfillmentId
);
event SolanaBridgeMarkedFailed(bytes32 indexed requestId, string reason);
event SolanaBridgeRecovered(bytes32 indexed requestId, BridgeStatus newStatus, string note);
constructor(address admin) {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(BRIDGE_OPERATOR_ROLE, admin);
_grantRole(ORACLE_ROLE, admin);
isActive = true;
minFinalitySlots = 32;
}
function getChainType() external pure override returns (string memory) {
return "Solana";
}
function getChainIdentifier() external pure override returns (uint256 chainId, string memory identifier) {
return (0, "Solana-Mainnet");
}
function validateDestination(bytes calldata destination) external pure override returns (bool) {
uint256 length = destination.length;
return length >= 32 && length <= 44;
}
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 Solana 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 SolanaBridgeInitiated(
requestId,
msg.sender,
token,
amount,
string(destination),
recipient
);
return requestId;
}
function confirmTransaction(
bytes32 requestId,
string calldata txSignature,
uint256 finalizedSlot,
bytes32 fulfillmentId
) external onlyRole(ORACLE_ROLE) {
BridgeRequest storage request = bridgeRequests[requestId];
require(request.status == BridgeStatus.Locked, "Invalid status");
require(bytes(txSignature).length > 0, "Missing Solana signature");
require(finalizedSlot >= minFinalitySlots, "Insufficient finality");
require(fulfillmentId != bytes32(0), "Missing fulfillment id");
require(!usedFulfillmentIds[fulfillmentId], "Fulfillment already used");
usedFulfillmentIds[fulfillmentId] = true;
fulfillmentIdsByRequest[requestId] = fulfillmentId;
solanaTxSignatures[requestId] = txSignature;
confirmedSlots[requestId] = finalizedSlot;
request.status = BridgeStatus.Confirmed;
request.completedAt = block.timestamp;
emit SolanaBridgeConfirmed(requestId, txSignature, finalizedSlot, 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 SolanaBridgeMarkedFailed(requestId, reason);
}
function recoverBridge(
bytes32 requestId,
BridgeStatus newStatus,
string calldata note
) external onlyRole(BRIDGE_OPERATOR_ROLE) {
BridgeRequest storage request = bridgeRequests[requestId];
require(request.requestId != bytes32(0), "Unknown request");
require(
request.status == BridgeStatus.Failed || request.status == BridgeStatus.Confirmed,
"Recovery not allowed"
);
request.status = newStatus;
request.completedAt = block.timestamp;
emit SolanaBridgeRecovered(requestId, newStatus, note);
}
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 setMinFinalitySlots(uint256 slots) external onlyRole(DEFAULT_ADMIN_ROLE) {
require(slots > 0, "Zero finality");
minFinalitySlots = slots;
}
}

View File

@@ -0,0 +1,181 @@
// 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_;
}
}