feat: expand non-evm relay and route planning support
This commit is contained in:
209
contracts/bridge/adapters/non-evm/SolanaAdapter.sol
Normal file
209
contracts/bridge/adapters/non-evm/SolanaAdapter.sol
Normal 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;
|
||||
}
|
||||
}
|
||||
181
contracts/bridge/adapters/non-evm/TronAdapter.sol
Normal file
181
contracts/bridge/adapters/non-evm/TronAdapter.sol
Normal 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_;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user