Files
smom-dbis-138/test/bridge/integration/MultiChainBridge.t.sol
2026-03-02 12:14:09 -08:00

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();
}
}