- CCIP/trustless bridge contracts, GRU tokens, DEX/PMM tests, reserve vault. - Token-aggregation service routes, planner, chain config, relay env templates. - Config snapshots and multi-chain deployment markdown updates. - gitignore services/btc-intake/dist/ (tsc output); do not track dist. Run forge build && forge test before deploy (large solc graph). Made-with: Cursor
272 lines
12 KiB
Solidity
272 lines
12 KiB
Solidity
// 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
|
|
})
|
|
);
|
|
}
|
|
}
|