189 lines
6.2 KiB
Solidity
189 lines
6.2 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";
|
|
|
|
/**
|
|
* @title XRPLAdapter
|
|
* @notice Bridge adapter for XRP Ledger (XRPL)
|
|
* @dev Uses oracle/relayer pattern for non-EVM chain integration
|
|
*/
|
|
contract XRPLAdapter 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;
|
|
|
|
mapping(bytes32 => BridgeRequest) public bridgeRequests;
|
|
mapping(bytes32 => bytes32) public xrplTxHashes; // requestId => xrplTxHash
|
|
mapping(address => uint256) public nonces;
|
|
|
|
// XRPL address validation: starts with 'r', 25-35 chars
|
|
mapping(string => bool) public validatedXRPLAddresses;
|
|
|
|
event XRPLBridgeInitiated(
|
|
bytes32 indexed requestId,
|
|
address indexed sender,
|
|
address indexed token,
|
|
uint256 amount,
|
|
string xrplDestination,
|
|
uint32 destinationTag
|
|
);
|
|
|
|
event XRPLBridgeConfirmed(
|
|
bytes32 indexed requestId,
|
|
bytes32 indexed xrplTxHash,
|
|
uint256 ledgerIndex
|
|
);
|
|
|
|
constructor(address admin) {
|
|
_grantRole(DEFAULT_ADMIN_ROLE, admin);
|
|
_grantRole(BRIDGE_OPERATOR_ROLE, admin);
|
|
_grantRole(ORACLE_ROLE, admin);
|
|
isActive = true;
|
|
}
|
|
|
|
function getChainType() external pure override returns (string memory) {
|
|
return "XRPL";
|
|
}
|
|
|
|
function getChainIdentifier() external pure override returns (uint256 chainId, string memory identifier) {
|
|
return (0, "XRPL-Mainnet");
|
|
}
|
|
|
|
function validateDestination(bytes calldata destination) external pure override returns (bool) {
|
|
string memory addr = string(destination);
|
|
bytes memory addrBytes = bytes(addr);
|
|
|
|
// XRPL addresses: r + 25-34 base58 chars
|
|
if (addrBytes.length < 26 || addrBytes.length > 35) return false;
|
|
if (addrBytes[0] != 'r') return false;
|
|
|
|
// Basic validation - full validation requires base58 decoding
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @notice Convert XRP drops to wei (1 XRP = 1,000,000 drops)
|
|
*/
|
|
function dropsToWei(uint64 drops) public pure returns (uint256) {
|
|
return uint256(drops) * 1e12; // 1 drop = 0.000001 XRP, 1 XRP = 1e18 wei
|
|
}
|
|
|
|
/**
|
|
* @notice Convert wei to XRP drops
|
|
*/
|
|
function weiToDrops(uint256 weiAmount) public pure returns (uint64) {
|
|
return uint64(weiAmount / 1e12);
|
|
}
|
|
|
|
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 XRPL address");
|
|
|
|
string memory xrplDestination = string(destination);
|
|
uint32 destinationTag = 0;
|
|
|
|
// Parse destination tag if provided in recipient bytes
|
|
if (recipient.length >= 4) {
|
|
destinationTag = abi.decode(recipient, (uint32));
|
|
}
|
|
|
|
requestId = keccak256(abi.encodePacked(
|
|
msg.sender,
|
|
token,
|
|
amount,
|
|
xrplDestination,
|
|
destinationTag,
|
|
nonces[msg.sender]++,
|
|
block.timestamp
|
|
));
|
|
|
|
// Lock tokens on EVM side
|
|
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 XRPLBridgeInitiated(requestId, msg.sender, token, amount, xrplDestination, destinationTag);
|
|
|
|
return requestId;
|
|
}
|
|
|
|
/**
|
|
* @notice Oracle confirms XRPL transaction
|
|
*/
|
|
function confirmXRPLTransaction(
|
|
bytes32 requestId,
|
|
bytes32 xrplTxHash,
|
|
uint256 ledgerIndex
|
|
) external onlyRole(ORACLE_ROLE) {
|
|
BridgeRequest storage request = bridgeRequests[requestId];
|
|
require(request.status == BridgeStatus.Locked, "Invalid status");
|
|
|
|
request.status = BridgeStatus.Confirmed;
|
|
request.completedAt = block.timestamp;
|
|
xrplTxHashes[requestId] = xrplTxHash;
|
|
|
|
emit XRPLBridgeConfirmed(requestId, xrplTxHash, ledgerIndex);
|
|
}
|
|
|
|
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");
|
|
|
|
// Refund tokens
|
|
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 pure override returns (uint256 fee) {
|
|
// XRPL fees are very low (~0.000012 XRP per transaction)
|
|
return 12000; // 0.000012 XRP in drops
|
|
}
|
|
|
|
function setIsActive(bool _isActive) external onlyRole(DEFAULT_ADMIN_ROLE) {
|
|
isActive = _isActive;
|
|
}
|
|
}
|