// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "forge-std/Test.sol"; import "../../../contracts/bridge/atomic/AtomicBridgeCoordinator.sol"; import "../../../contracts/bridge/atomic/AtomicFeePolicy.sol"; import "../../../contracts/bridge/atomic/AtomicFulfillerRegistry.sol"; import "../../../contracts/bridge/atomic/AtomicLiquidityVault.sol"; import "../../../contracts/bridge/atomic/AtomicObligationEscrow.sol"; import "../../../contracts/bridge/atomic/AtomicQuoteEngine.sol"; import "../../../contracts/bridge/atomic/AtomicSettlementRouter.sol"; import "../../../contracts/bridge/atomic/AtomicSlashingManager.sol"; import "../../../contracts/bridge/atomic/AtomicTypes.sol"; import "../../../contracts/bridge/atomic/interfaces/IAtomicSettlementAdapter.sol"; import "../../dbis/MockMintableToken.sol"; contract MockAtomicSettlementAdapter is IAtomicSettlementAdapter { address public lastToken; uint256 public lastAmount; address public lastRecipient; bytes public lastData; function executeSettlement( bytes32 obligationId, address token, uint256 amount, address recipient, bytes calldata data ) external payable returns (bytes32 settlementId) { lastToken = token; lastAmount = amount; lastRecipient = recipient; lastData = data; settlementId = keccak256(abi.encode(obligationId, token, amount, recipient, data, block.timestamp)); } } contract AtomicBridgeCoordinatorTest is Test { bytes32 internal constant MOCK_SETTLEMENT_MODE = keccak256("MOCK_SETTLEMENT_MODE"); MockMintableToken internal sourceToken; MockMintableToken internal destinationToken; MockMintableToken internal bondToken; AtomicLiquidityVault internal vault; AtomicFulfillerRegistry internal registry; AtomicFeePolicy internal feePolicy; AtomicObligationEscrow internal escrow; AtomicSettlementRouter internal router; AtomicSlashingManager internal slashingManager; AtomicBridgeCoordinator internal coordinator; AtomicQuoteEngine internal quoteEngine; MockAtomicSettlementAdapter internal settlementAdapter; address internal user = address(0x1111); address internal fulfiller = address(0x2222); address internal protocolTreasury = address(0x3333); bytes32 internal corridorId; function setUp() public { sourceToken = new MockMintableToken("Chain 138 USD", "cUSDC", 6, address(this)); destinationToken = new MockMintableToken("Mainnet Wrapped USD", "cWUSDC", 6, address(this)); bondToken = new MockMintableToken("Bond USDC", "bUSDC", 6, address(this)); vault = new AtomicLiquidityVault(address(this)); registry = new AtomicFulfillerRegistry(address(bondToken), address(this)); feePolicy = new AtomicFeePolicy(address(this)); escrow = new AtomicObligationEscrow(address(this)); router = new AtomicSettlementRouter(address(this)); slashingManager = new AtomicSlashingManager(address(registry), address(this)); coordinator = new AtomicBridgeCoordinator( address(vault), address(registry), address(escrow), address(router), address(feePolicy), address(slashingManager), protocolTreasury, address(this) ); quoteEngine = new AtomicQuoteEngine( address(coordinator), address(vault), address(registry), address(feePolicy) ); settlementAdapter = new MockAtomicSettlementAdapter(); vault.grantRole(vault.COORDINATOR_ROLE(), address(coordinator)); vault.grantRole(vault.RECONCILER_ROLE(), address(coordinator)); registry.grantRole(registry.COORDINATOR_ROLE(), address(coordinator)); registry.grantRole(registry.SLASHER_ROLE(), address(slashingManager)); escrow.grantRole(escrow.COORDINATOR_ROLE(), address(coordinator)); router.grantRole(router.COORDINATOR_ROLE(), address(coordinator)); slashingManager.grantRole(slashingManager.COORDINATOR_ROLE(), address(coordinator)); router.setAdapter(MOCK_SETTLEMENT_MODE, address(settlementAdapter)); corridorId = coordinator.getCorridorId(138, 1, address(sourceToken), address(destinationToken)); coordinator.configureCorridor( AtomicTypes.CorridorConfig({ enabled: true, degraded: false, sourceChain: 138, destinationChain: 1, assetIn: address(sourceToken), assetOut: address(destinationToken), maxNotional: 500_000e6, maxReservedBps: 5_000, targetBuffer: 10_000e6, maxSettlementBacklog: 250_000e6, maxOracleDriftBps: 500, fulfilmentTimeout: 1 days, settlementTimeout: 2 days, defaultSettlementMode: MOCK_SETTLEMENT_MODE }) ); feePolicy.setCorridorPolicy(corridorId, 100, 50, 12_000, 1_000, 1 days, 2 days); vault.setTargetBuffer(corridorId, address(destinationToken), 10_000e6); registry.setFulfillerActive(fulfiller, true); registry.setCorridorAuthorization(fulfiller, corridorId, true); destinationToken.mint(address(this), 100_000e6); destinationToken.approve(address(vault), type(uint256).max); vault.fundCorridor(corridorId, address(destinationToken), 100_000e6); bondToken.mint(fulfiller, 100_000e6); vm.startPrank(fulfiller); bondToken.approve(address(registry), type(uint256).max); registry.depositBond(50_000e6); vm.stopPrank(); sourceToken.mint(user, 50_000e6); vm.prank(user); sourceToken.approve(address(escrow), type(uint256).max); } function testCreateIntentReservesDestinationLiquidity() public { bytes32 obligationId = _createIntent(1_000e6, 900e6, block.timestamp + 1 hours); AtomicTypes.AtomicObligation memory obligation = coordinator.getObligation(obligationId); assertEq(uint8(obligation.status), uint8(AtomicTypes.ObligationStatus.IntentCreated)); assertEq(obligation.sourceEscrow, 1_000e6); assertEq(obligation.destinationReserve, 900e6); AtomicTypes.CorridorLiquidityState memory state = vault.getCorridorLiquidityState(corridorId, address(destinationToken)); assertEq(state.totalLiquidity, 100_000e6); assertEq(state.reservedLiquidity, 900e6); assertEq(state.freeLiquidity, 99_100e6); } function testQuoteReflectsExecutionReadyPath() public { AtomicTypes.AtomicQuote memory q = quoteEngine.quote(corridorId, 1_000e6, 900e6, fulfiller); assertEq(uint8(q.routeClass), uint8(AtomicTypes.RouteClass.ExecutionReady)); assertTrue(q.fulfillerAuthorized); assertTrue(q.fulfillerBondSufficient); assertEq(q.protocolFee, 5e6); assertEq(q.fulfillerFee, 10e6); assertEq(q.requiredBond, 1_080e6); } function testCommitFailsWhenBondIsInsufficient() public { address weakFulfiller = address(0x4444); registry.setFulfillerActive(weakFulfiller, true); registry.setCorridorAuthorization(weakFulfiller, corridorId, true); bondToken.mint(weakFulfiller, 100e6); vm.startPrank(weakFulfiller); bondToken.approve(address(registry), type(uint256).max); registry.depositBond(100e6); vm.stopPrank(); bytes32 obligationId = _createIntent(1_000e6, 900e6, block.timestamp + 1 hours); vm.prank(weakFulfiller); vm.expectRevert(AtomicFulfillerRegistry.InsufficientAvailableBond.selector); coordinator.submitCommitment(obligationId, bytes32(0)); } function testSuccessfulSettlementReleasesBondAndReplenishesLiquidity() public { bytes32 obligationId = _createIntent(1_000e6, 900e6, block.timestamp + 1 hours); vm.prank(fulfiller); coordinator.submitCommitment(obligationId, bytes32(0)); assertEq(destinationToken.balanceOf(user), 900e6); AtomicTypes.CorridorLiquidityState memory postFulfill = vault.getCorridorLiquidityState(corridorId, address(destinationToken)); assertEq(postFulfill.totalLiquidity, 99_100e6); assertEq(postFulfill.settlementBacklog, 900e6); bytes memory settlementData = abi.encodePacked(bytes32(uint256(123))); bytes32 settlementId = coordinator.initiateSettlement(obligationId, settlementData); assertTrue(settlementId != bytes32(0)); assertEq(settlementAdapter.lastToken(), address(sourceToken)); assertEq(settlementAdapter.lastAmount(), 985e6); assertEq(settlementAdapter.lastRecipient(), user); assertEq(sourceToken.balanceOf(protocolTreasury), 5e6); assertEq(sourceToken.balanceOf(fulfiller), 10e6); destinationToken.mint(address(this), 900e6); destinationToken.approve(address(vault), 900e6); coordinator.confirmSettlement(obligationId, 900e6); AtomicTypes.AtomicObligation memory obligation = coordinator.getObligation(obligationId); assertEq(uint8(obligation.status), uint8(AtomicTypes.ObligationStatus.Settled)); assertEq(registry.lockedBond(fulfiller), 0); AtomicTypes.CorridorLiquidityState memory postSettlement = vault.getCorridorLiquidityState(corridorId, address(destinationToken)); assertEq(postSettlement.totalLiquidity, 100_000e6); assertEq(postSettlement.settlementBacklog, 0); } function testExpiredIntentRefundsAndReleasesReservation() public { uint256 payerBalanceBefore = sourceToken.balanceOf(user); bytes32 obligationId = _createIntent(1_000e6, 900e6, block.timestamp + 30 minutes); vm.warp(block.timestamp + 31 minutes); coordinator.refundExpiredIntent(obligationId); AtomicTypes.AtomicObligation memory obligation = coordinator.getObligation(obligationId); assertEq(uint8(obligation.status), uint8(AtomicTypes.ObligationStatus.Refunded)); assertEq(sourceToken.balanceOf(user), payerBalanceBefore); AtomicTypes.CorridorLiquidityState memory state = vault.getCorridorLiquidityState(corridorId, address(destinationToken)); assertEq(state.reservedLiquidity, 0); } function testSettlementTimeoutSlashesBondAndDegradesCorridor() public { bytes32 obligationId = _createIntent(1_000e6, 900e6, block.timestamp + 1 hours); vm.prank(fulfiller); coordinator.submitCommitment(obligationId, bytes32(0)); coordinator.initiateSettlement(obligationId, abi.encodePacked(bytes32(uint256(456)))); uint256 treasuryBondBefore = bondToken.balanceOf(protocolTreasury); vm.warp(block.timestamp + 3 days); coordinator.handleSettlementTimeout(obligationId); AtomicTypes.AtomicObligation memory obligation = coordinator.getObligation(obligationId); assertEq(uint8(obligation.status), uint8(AtomicTypes.ObligationStatus.Slashed)); AtomicTypes.CorridorConfig memory cfg = coordinator.getCorridorConfig(corridorId); assertTrue(cfg.degraded); assertEq(bondToken.balanceOf(protocolTreasury), treasuryBondBefore + 1_080e6); } function testConcurrentReservationsDoNotOverReserveLiquidity() public { _createIntent(10_000e6, 40_000e6, block.timestamp + 1 hours); _createIntent(10_000e6, 10_000e6, block.timestamp + 1 hours); vm.expectRevert(AtomicBridgeCoordinator.ReservedLiquidityLimitExceeded.selector); _createIntent(10_000e6, 5_000e6, block.timestamp + 1 hours); } function _createIntent(uint256 amountIn, uint256 minAmountOut, uint256 deadline) internal returns (bytes32) { vm.prank(user); return coordinator.createIntent( AtomicBridgeCoordinator.CreateIntentParams({ sourceChain: 138, destinationChain: 1, assetIn: address(sourceToken), assetOut: address(destinationToken), amountIn: amountIn, minAmountOut: minAmountOut, recipient: user, deadline: deadline, routeId: corridorId }) ); } }