311 lines
12 KiB
Solidity
311 lines
12 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.20;
|
|
|
|
import {Test} from "forge-std/Test.sol";
|
|
import "../../../contracts/bridge/UniversalCCIPBridge.sol";
|
|
import "../../../contracts/bridge/adapters/evm/EVMAdapter.sol";
|
|
import "../../../contracts/bridge/adapters/evm/XDCAdapter.sol";
|
|
import "../../../contracts/bridge/adapters/non-evm/XRPLAdapter.sol";
|
|
import "../../../contracts/registry/ChainRegistry.sol";
|
|
import "../../../contracts/registry/UniversalAssetRegistry.sol";
|
|
import "../../../contracts/ccip/IRouterClient.sol";
|
|
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
|
|
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|
|
|
/** Mock CCIP router for tests; production uses existing Chainlink CCIP routers via CCIP_ROUTER in .env */
|
|
contract MockCCIPRouter is IRouterClient {
|
|
function getFee(uint64, EVM2AnyMessage memory) external pure override returns (uint256) {
|
|
return 0;
|
|
}
|
|
function ccipSend(uint64, EVM2AnyMessage memory) external payable override returns (bytes32 messageId, uint256) {
|
|
messageId = keccak256(abi.encodePacked(block.timestamp, msg.sender));
|
|
return (messageId, 0);
|
|
}
|
|
function getSupportedTokens(uint64) external pure override returns (address[] memory) {
|
|
return new address[](0);
|
|
}
|
|
}
|
|
|
|
contract MockTestToken is ERC20 {
|
|
constructor() ERC20("Test Token", "TEST") {
|
|
_mint(msg.sender, 1000000 ether);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @title MultiChainBridgeIntegrationTest
|
|
* @notice Integration tests for multi-chain bridge functionality
|
|
* @dev Tests cross-chain transfers, adapter functionality, and multi-chain scenarios.
|
|
* Asset registration uses UniversalAssetRegistry proposeAsset → voteOnProposal → executeProposal flow.
|
|
*/
|
|
contract MultiChainBridgeIntegrationTest is Test {
|
|
UniversalCCIPBridge public bridge;
|
|
ChainRegistry public chainRegistry;
|
|
UniversalAssetRegistry public assetRegistry;
|
|
|
|
// Adapters
|
|
EVMAdapter public polygonAdapter;
|
|
EVMAdapter public arbitrumAdapter;
|
|
XDCAdapter public xdcAdapter;
|
|
XRPLAdapter public xrplAdapter;
|
|
|
|
// Test tokens
|
|
MockTestToken public testToken;
|
|
|
|
address public admin = address(0x1);
|
|
address public user = address(0x2);
|
|
address public recipient = address(0x3);
|
|
|
|
// Chain IDs
|
|
uint256 public constant POLYGON_CHAIN_ID = 137;
|
|
uint256 public constant ARBITRUM_CHAIN_ID = 42161;
|
|
uint256 public constant XDC_CHAIN_ID = 50;
|
|
|
|
function setUp() public {
|
|
vm.startPrank(admin);
|
|
|
|
// Deploy Universal Asset Registry (upgradeable: use proxy)
|
|
UniversalAssetRegistry regImpl = new UniversalAssetRegistry();
|
|
bytes memory regInitData = abi.encodeWithSelector(UniversalAssetRegistry.initialize.selector, admin);
|
|
ERC1967Proxy regProxy = new ERC1967Proxy(address(regImpl), regInitData);
|
|
assetRegistry = UniversalAssetRegistry(address(regProxy));
|
|
|
|
// Add admin as validator so quorum can be met
|
|
assetRegistry.addValidator(admin);
|
|
|
|
// Deploy Chain Registry (upgradeable: use proxy)
|
|
ChainRegistry chainImpl = new ChainRegistry();
|
|
bytes memory chainInitData = abi.encodeWithSelector(ChainRegistry.initialize.selector, admin);
|
|
ERC1967Proxy chainProxy = new ERC1967Proxy(address(chainImpl), chainInitData);
|
|
chainRegistry = ChainRegistry(address(chainProxy));
|
|
|
|
// Deploy Universal CCIP Bridge (upgradeable: use proxy)
|
|
// Use mock CCIP router in tests; production uses existing Chainlink routers via CCIP_ROUTER in .env
|
|
MockCCIPRouter mockRouter = new MockCCIPRouter();
|
|
UniversalCCIPBridge bridgeImpl = new UniversalCCIPBridge();
|
|
bytes memory bridgeInitData = abi.encodeWithSelector(
|
|
UniversalCCIPBridge.initialize.selector,
|
|
address(assetRegistry),
|
|
address(mockRouter),
|
|
admin
|
|
);
|
|
ERC1967Proxy bridgeProxy = new ERC1967Proxy(address(bridgeImpl), bridgeInitData);
|
|
bridge = UniversalCCIPBridge(payable(address(bridgeProxy)));
|
|
|
|
// Deploy test token
|
|
testToken = new MockTestToken();
|
|
require(testToken.transfer(user, 1000000e18), "transfer failed");
|
|
|
|
// Register test token via propose → vote → timelock → execute
|
|
bytes32 proposalId = assetRegistry.proposeAsset(
|
|
address(testToken),
|
|
UniversalAssetRegistry.AssetType.ERC20Standard,
|
|
UniversalAssetRegistry.ComplianceLevel.Public,
|
|
"Test Token",
|
|
"TEST",
|
|
18,
|
|
"",
|
|
50, // volatilityScore
|
|
0, // minBridge
|
|
1e24 // maxBridge
|
|
);
|
|
assetRegistry.voteOnProposal(proposalId, true);
|
|
UniversalAssetRegistry.PendingAssetProposal memory p = assetRegistry.getProposal(proposalId);
|
|
vm.warp(p.executeAfter + 1);
|
|
assetRegistry.executeProposal(proposalId);
|
|
|
|
// Enable bridge destinations for test token (mock receiver for tests)
|
|
address mockReceiver = address(0xbeef);
|
|
// forge-lint: disable-next-line(unsafe-typecast) -- chain IDs fit in uint64
|
|
bridge.addDestination(address(testToken), uint64(POLYGON_CHAIN_ID), mockReceiver);
|
|
// forge-lint: disable-next-line(unsafe-typecast) -- chain IDs fit in uint64
|
|
bridge.addDestination(address(testToken), uint64(ARBITRUM_CHAIN_ID), mockReceiver);
|
|
|
|
// Deploy adapters
|
|
polygonAdapter = new EVMAdapter(admin, payable(address(bridge)), POLYGON_CHAIN_ID, "Polygon");
|
|
arbitrumAdapter = new EVMAdapter(admin, payable(address(bridge)), ARBITRUM_CHAIN_ID, "Arbitrum");
|
|
xdcAdapter = new XDCAdapter(admin, payable(address(bridge)));
|
|
xrplAdapter = new XRPLAdapter(admin);
|
|
|
|
// Register chains
|
|
chainRegistry.registerEVMChain(
|
|
POLYGON_CHAIN_ID,
|
|
address(polygonAdapter),
|
|
"https://polygonscan.com",
|
|
12,
|
|
2,
|
|
""
|
|
);
|
|
|
|
chainRegistry.registerEVMChain(
|
|
ARBITRUM_CHAIN_ID,
|
|
address(arbitrumAdapter),
|
|
"https://arbiscan.io",
|
|
12,
|
|
2,
|
|
""
|
|
);
|
|
|
|
chainRegistry.registerEVMChain(
|
|
XDC_CHAIN_ID,
|
|
address(xdcAdapter),
|
|
"https://explorer.xdc.network",
|
|
12,
|
|
2,
|
|
""
|
|
);
|
|
|
|
chainRegistry.registerNonEVMChain(
|
|
"XRPL-Mainnet",
|
|
ChainRegistry.ChainType.XRPL,
|
|
address(xrplAdapter),
|
|
"https://xrpscan.com",
|
|
1,
|
|
4,
|
|
true,
|
|
""
|
|
);
|
|
|
|
vm.stopPrank();
|
|
}
|
|
|
|
function testEVMToEVMBridge() public {
|
|
vm.startPrank(user);
|
|
|
|
uint256 amount = 1000e18;
|
|
testToken.approve(address(polygonAdapter), amount);
|
|
|
|
// Bridge to Polygon
|
|
bytes memory destination = abi.encodePacked(recipient);
|
|
bytes memory recipientData = abi.encode(recipient);
|
|
|
|
bytes32 requestId = polygonAdapter.bridge(
|
|
address(testToken),
|
|
amount,
|
|
destination,
|
|
recipientData
|
|
);
|
|
|
|
assertNotEq(requestId, bytes32(0), "Request ID should not be zero");
|
|
|
|
// Check bridge status
|
|
IChainAdapter.BridgeRequest memory request = polygonAdapter.getBridgeStatus(requestId);
|
|
assertTrue(request.status == IChainAdapter.BridgeStatus.Locked, "Status should be Locked");
|
|
assertEq(request.amount, amount, "Amount should match");
|
|
assertEq(request.sender, user, "Sender should match");
|
|
|
|
vm.stopPrank();
|
|
}
|
|
|
|
function testXDCAddressConversion() public view {
|
|
// Round-trip using adapter output so format/length (43 chars: "xdc" + 40 hex) is correct
|
|
address ethAddr = address(0x742d35Cc6634C0532925a3b844Bc9e7595f0bE);
|
|
string memory xdcAddr = xdcAdapter.convertEthToXdc(ethAddr);
|
|
assertEq(bytes(xdcAddr).length, 43, "XDC string should be 43 chars");
|
|
address backToEth = xdcAdapter.convertXdcToEth(xdcAddr);
|
|
assertEq(backToEth, ethAddr, "Round-trip XDC conversion should match");
|
|
}
|
|
|
|
function testXRPLBridge() public {
|
|
vm.startPrank(user);
|
|
|
|
uint256 amount = 1000e18;
|
|
testToken.approve(address(xrplAdapter), amount);
|
|
|
|
// Bridge to XRPL (adapter expects raw string bytes for destination; pass tag as ABI-encoded or skip so length < 4)
|
|
string memory xrplDestination = "rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH";
|
|
bytes memory destination = bytes(xrplDestination);
|
|
bytes memory recipientData = ""; // no destination tag; avoid abi.decode of raw string in adapter
|
|
|
|
bytes32 requestId = xrplAdapter.bridge(
|
|
address(testToken),
|
|
amount,
|
|
destination,
|
|
recipientData
|
|
);
|
|
|
|
assertNotEq(requestId, bytes32(0), "Request ID should not be zero");
|
|
|
|
// Simulate oracle confirmation
|
|
vm.stopPrank();
|
|
vm.prank(admin);
|
|
xrplAdapter.confirmXRPLTransaction(
|
|
requestId,
|
|
keccak256("xrpl-tx-hash"),
|
|
12345
|
|
);
|
|
|
|
IChainAdapter.BridgeRequest memory request = xrplAdapter.getBridgeStatus(requestId);
|
|
assertTrue(request.status == IChainAdapter.BridgeStatus.Confirmed, "Status should be Confirmed");
|
|
|
|
vm.stopPrank();
|
|
}
|
|
|
|
function testMultiChainRouting() public {
|
|
// Test that chain registry correctly routes to appropriate adapter
|
|
ChainRegistry.ChainMetadata memory polygonMeta = chainRegistry.getEVMChain(POLYGON_CHAIN_ID);
|
|
assertEq(polygonMeta.adapter, address(polygonAdapter), "Should return Polygon adapter");
|
|
assertTrue(polygonMeta.isActive, "Chain should be active");
|
|
|
|
ChainRegistry.ChainMetadata memory arbitrumMeta = chainRegistry.getEVMChain(ARBITRUM_CHAIN_ID);
|
|
assertEq(arbitrumMeta.adapter, address(arbitrumAdapter), "Should return Arbitrum adapter");
|
|
assertTrue(arbitrumMeta.isActive, "Chain should be active");
|
|
}
|
|
|
|
function testAdapterValidation() public {
|
|
// Test destination validation
|
|
bytes memory validEvm = abi.encodePacked(recipient);
|
|
assertTrue(polygonAdapter.validateDestination(validEvm), "Valid EVM address should pass");
|
|
|
|
bytes memory invalidEvm = abi.encodePacked(address(0));
|
|
assertFalse(polygonAdapter.validateDestination(invalidEvm), "Invalid address should fail");
|
|
|
|
// Test XRPL validation (adapter expects raw string bytes, not ABI-encoded)
|
|
string memory validXrpl = "rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH";
|
|
assertTrue(xrplAdapter.validateDestination(bytes(validXrpl)), "Valid XRPL address should pass");
|
|
}
|
|
|
|
function testFeeEstimation() public {
|
|
bytes memory destination = abi.encodePacked(recipient);
|
|
|
|
uint256 polygonFee = polygonAdapter.estimateFee(address(testToken), 1000e18, destination);
|
|
assertGt(polygonFee, 0, "Polygon fee should be greater than zero");
|
|
|
|
uint256 xrplFee = xrplAdapter.estimateFee(address(testToken), 1000e18, destination);
|
|
assertGt(xrplFee, 0, "XRPL fee should be greater than zero");
|
|
}
|
|
|
|
function testCancelBridge() public {
|
|
vm.startPrank(user);
|
|
|
|
uint256 amount = 1000e18;
|
|
testToken.approve(address(polygonAdapter), amount);
|
|
|
|
bytes memory destination = abi.encodePacked(recipient);
|
|
bytes memory recipientData = abi.encode(recipient);
|
|
|
|
bytes32 requestId = polygonAdapter.bridge(
|
|
address(testToken),
|
|
amount,
|
|
destination,
|
|
recipientData
|
|
);
|
|
|
|
// Tokens were pulled to the universal bridge; adapter.cancelBridge() refunds from adapter balance.
|
|
// Simulate bridge not having consumed tokens yet by sending them back to the adapter.
|
|
vm.stopPrank();
|
|
vm.prank(address(bridge));
|
|
require(testToken.transfer(address(polygonAdapter), amount), "transfer failed");
|
|
vm.startPrank(user);
|
|
|
|
// Cancel bridge (adapter now has tokens and can refund user)
|
|
bool cancelled = polygonAdapter.cancelBridge(requestId);
|
|
assertTrue(cancelled, "Bridge should be cancelled");
|
|
|
|
IChainAdapter.BridgeRequest memory request = polygonAdapter.getBridgeStatus(requestId);
|
|
assertTrue(request.status == IChainAdapter.BridgeStatus.Cancelled, "Status should be Cancelled");
|
|
|
|
vm.stopPrank();
|
|
}
|
|
}
|