// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { AaveQuotePushFlashReceiver, IAaveExternalUnwinder } from "../src/AaveQuotePushFlashReceiver.sol"; import {AtomicBridgeCoordinator} from "atomic/AtomicBridgeCoordinator.sol"; import {AtomicFeePolicy} from "atomic/AtomicFeePolicy.sol"; import {AtomicFulfillerRegistry} from "atomic/AtomicFulfillerRegistry.sol"; import {AtomicLiquidityVault} from "atomic/AtomicLiquidityVault.sol"; import {AtomicObligationEscrow} from "atomic/AtomicObligationEscrow.sol"; import {AtomicSettlementRouter} from "atomic/AtomicSettlementRouter.sol"; import {AtomicSlashingManager} from "atomic/AtomicSlashingManager.sol"; import {AtomicTypes} from "atomic/AtomicTypes.sol"; import {IAtomicSettlementAdapter} from "atomic/interfaces/IAtomicSettlementAdapter.sol"; import {MockMintableToken} from "repo-test/dbis/MockMintableToken.sol"; contract AaveForkMockExternalUnwinder is IAaveExternalUnwinder { IERC20 public immutable base; IERC20 public immutable quote; uint256 public immutable numerator; uint256 public immutable denominator; constructor(IERC20 base_, IERC20 quote_, uint256 numerator_, uint256 denominator_) { base = base_; quote = quote_; numerator = numerator_; denominator = denominator_; } function unwind(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, bytes calldata) external override returns (uint256 amountOut) { require(tokenIn == address(base), "base only"); require(tokenOut == address(quote), "quote only"); IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn); amountOut = amountIn * numerator / denominator; require(amountOut >= minAmountOut, "min unwind"); IERC20(address(quote)).transfer(msg.sender, amountOut); } } contract MockAtomicSettlementAdapter is IAtomicSettlementAdapter { address public lastToken; uint256 public lastAmount; address public lastRecipient; bytes32 public lastObligationId; function executeSettlement( bytes32 obligationId, address token, uint256 amount, address recipient, bytes calldata ) external payable returns (bytes32 settlementId) { lastObligationId = obligationId; lastToken = token; lastAmount = amount; lastRecipient = recipient; settlementId = keccak256(abi.encode(obligationId, token, amount, recipient, block.timestamp)); } } contract AaveQuotePushFlashReceiverMainnetForkTest is Test { address constant AAVE_POOL_MAINNET = 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2; address constant DODO_PMM_INTEGRATION_MAINNET = 0xa9F284eD010f4F7d7F8F201742b49b9f58e29b84; address constant POOL_CWUSDC_USDC = 0x69776fc607e9edA8042e320e7e43f54d06c68f0E; address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; address constant CWUSDC = 0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a; bytes32 constant MOCK_SETTLEMENT_MODE = keccak256("MOCK_SETTLEMENT_MODE"); AaveQuotePushFlashReceiver internal receiver; AaveForkMockExternalUnwinder internal unwinder; MockMintableToken internal cusdc138; MockMintableToken internal bondToken; AtomicLiquidityVault internal atomicVault; AtomicFulfillerRegistry internal atomicRegistry; AtomicFeePolicy internal atomicFeePolicy; AtomicObligationEscrow internal atomicEscrow; AtomicSettlementRouter internal atomicRouter; AtomicSlashingManager internal atomicSlashingManager; AtomicBridgeCoordinator internal atomicCoordinator; MockAtomicSettlementAdapter internal mockSettlementAdapter; bytes32 internal atomicCorridorId; address internal destinationRecipient = address(0x138138); function setUp() public { string memory rpcUrl = vm.envString("ETHEREUM_MAINNET_RPC"); vm.createSelectFork(rpcUrl); receiver = new AaveQuotePushFlashReceiver(AAVE_POOL_MAINNET); unwinder = new AaveForkMockExternalUnwinder(IERC20(CWUSDC), IERC20(USDC), 130, 100); deal(USDC, address(unwinder), 50_000_000_000); cusdc138 = new MockMintableToken("Chain 138 USDC", "cUSDC", 6, address(this)); bondToken = new MockMintableToken("Atomic Bond", "aBOND", 6, address(this)); atomicVault = new AtomicLiquidityVault(address(this)); atomicRegistry = new AtomicFulfillerRegistry(address(bondToken), address(this)); atomicFeePolicy = new AtomicFeePolicy(address(this)); atomicEscrow = new AtomicObligationEscrow(address(this)); atomicRouter = new AtomicSettlementRouter(address(this)); atomicSlashingManager = new AtomicSlashingManager(address(atomicRegistry), address(this)); mockSettlementAdapter = new MockAtomicSettlementAdapter(); atomicCoordinator = new AtomicBridgeCoordinator( address(atomicVault), address(atomicRegistry), address(atomicEscrow), address(atomicRouter), address(atomicFeePolicy), address(atomicSlashingManager), address(this), address(this) ); atomicVault.grantRole(atomicVault.COORDINATOR_ROLE(), address(atomicCoordinator)); atomicVault.grantRole(atomicVault.RECONCILER_ROLE(), address(atomicCoordinator)); atomicRegistry.grantRole(atomicRegistry.COORDINATOR_ROLE(), address(atomicCoordinator)); atomicRegistry.grantRole(atomicRegistry.SLASHER_ROLE(), address(atomicSlashingManager)); atomicEscrow.grantRole(atomicEscrow.COORDINATOR_ROLE(), address(atomicCoordinator)); atomicRouter.grantRole(atomicRouter.COORDINATOR_ROLE(), address(atomicCoordinator)); atomicSlashingManager.grantRole(atomicSlashingManager.COORDINATOR_ROLE(), address(atomicCoordinator)); atomicRouter.setAdapter(MOCK_SETTLEMENT_MODE, address(mockSettlementAdapter)); atomicCorridorId = atomicCoordinator.getCorridorId(1, 138, CWUSDC, address(cusdc138)); atomicCoordinator.configureCorridor( AtomicTypes.CorridorConfig({ enabled: true, degraded: false, sourceChain: 1, destinationChain: 138, assetIn: CWUSDC, assetOut: address(cusdc138), maxNotional: 10_000_000, maxReservedBps: 8_000, targetBuffer: 100_000, maxSettlementBacklog: 5_000_000, maxOracleDriftBps: 500, fulfilmentTimeout: 1 days, settlementTimeout: 2 days, defaultSettlementMode: MOCK_SETTLEMENT_MODE }) ); atomicFeePolicy.setCorridorPolicy(atomicCorridorId, 25, 10, 12_000, 500, 1 days, 2 days); atomicVault.setTargetBuffer(atomicCorridorId, address(cusdc138), 100_000); cusdc138.mint(address(this), 5_000_000); cusdc138.approve(address(atomicVault), type(uint256).max); atomicVault.fundCorridor(atomicCorridorId, address(cusdc138), 5_000_000); bondToken.mint(address(receiver), 5_000_000); vm.startPrank(address(receiver)); bondToken.approve(address(atomicRegistry), type(uint256).max); atomicRegistry.depositBond(3_000_000); vm.stopPrank(); atomicRegistry.setFulfillerActive(address(receiver), true); atomicRegistry.setCorridorAuthorization(address(receiver), atomicCorridorId, true); } function testFork_aaveQuotePush_usesRealAaveAndRealMainnetPmm() public { uint256 amount = 2_964_298; uint256 receiverQuoteBefore = IERC20(USDC).balanceOf(address(receiver)); uint256 poolBaseBefore = IERC20(CWUSDC).balanceOf(POOL_CWUSDC_USDC); uint256 poolQuoteBefore = IERC20(USDC).balanceOf(POOL_CWUSDC_USDC); AaveQuotePushFlashReceiver.QuotePushParams memory p = AaveQuotePushFlashReceiver.QuotePushParams({ integration: DODO_PMM_INTEGRATION_MAINNET, pmmPool: POOL_CWUSDC_USDC, baseToken: CWUSDC, externalUnwinder: address(unwinder), minOutPmm: 2_800_000, minOutUnwind: amount + 1_483, unwindData: bytes(""), atomicBridge: AaveQuotePushFlashReceiver.AtomicBridgeParams({ coordinator: address(0), sourceChain: 0, destinationChain: 0, destinationAsset: address(0), bridgeAmount: 0, minDestinationAmount: 0, destinationRecipient: address(0), destinationDeadline: 0, routeId: bytes32(0), settlementMode: bytes32(0), submitCommitment: false }) }); receiver.flashQuotePush(USDC, amount, p); uint256 receiverQuoteAfter = IERC20(USDC).balanceOf(address(receiver)); uint256 poolBaseAfter = IERC20(CWUSDC).balanceOf(POOL_CWUSDC_USDC); uint256 poolQuoteAfter = IERC20(USDC).balanceOf(POOL_CWUSDC_USDC); uint256 actualBaseOut = poolBaseBefore - poolBaseAfter; uint256 actualQuoteIntoPool = poolQuoteAfter - poolQuoteBefore; uint256 actualSurplus = receiverQuoteAfter - receiverQuoteBefore; uint256 premium = _aavePremium(amount); (uint256 predictedBaseOut, uint256 predictedUnwindOut, uint256 predictedSurplus) = _predictQuotePush(poolBaseBefore, poolQuoteBefore, amount, 3, 130, 100, 0, premium); uint256 predictedNetQuoteIn = _netQuoteIn(amount, 3); assertGt(receiverQuoteAfter, receiverQuoteBefore, "receiver retains surplus"); assertEq(IERC20(CWUSDC).balanceOf(address(receiver)), 0, "base fully unwound"); assertLt(poolBaseAfter, poolBaseBefore, "pool base decreased"); assertGt(poolQuoteAfter, poolQuoteBefore, "pool quote increased"); _assertWithinOnePercent(actualBaseOut, predictedBaseOut, "baseOut"); _assertWithinOnePercent(actualQuoteIntoPool, predictedNetQuoteIn, "netQuoteIn"); _assertWithinOnePercent(actualSurplus, predictedSurplus, "surplus"); assertApproxEqAbs(predictedUnwindOut, actualSurplus + amount + premium, 2, "unwindOut"); } function testFork_aaveQuotePush_atomicCorridorFulfillment_1_to_138_cwusdc_to_cusdc() public { uint256 amount = 2_964_298; uint256 bridgeAmount = 500_000; uint256 destinationRecipientBefore = cusdc138.balanceOf(destinationRecipient); uint256 poolBaseBefore = IERC20(CWUSDC).balanceOf(POOL_CWUSDC_USDC); uint256 poolQuoteBefore = IERC20(USDC).balanceOf(POOL_CWUSDC_USDC); AaveQuotePushFlashReceiver.QuotePushParams memory p = AaveQuotePushFlashReceiver.QuotePushParams({ integration: DODO_PMM_INTEGRATION_MAINNET, pmmPool: POOL_CWUSDC_USDC, baseToken: CWUSDC, externalUnwinder: address(unwinder), minOutPmm: 2_800_000, minOutUnwind: amount + 1_483, unwindData: bytes(""), atomicBridge: AaveQuotePushFlashReceiver.AtomicBridgeParams({ coordinator: address(atomicCoordinator), sourceChain: 1, destinationChain: 138, destinationAsset: address(cusdc138), bridgeAmount: bridgeAmount, minDestinationAmount: bridgeAmount, destinationRecipient: destinationRecipient, destinationDeadline: block.timestamp + 1 hours, routeId: atomicCorridorId, settlementMode: bytes32(0), submitCommitment: true }) }); uint256 receiverQuoteBefore = IERC20(USDC).balanceOf(address(receiver)); receiver.flashQuotePush(USDC, amount, p); uint256 receiverQuoteAfter = IERC20(USDC).balanceOf(address(receiver)); uint256 poolBaseAfter = IERC20(CWUSDC).balanceOf(POOL_CWUSDC_USDC); uint256 poolQuoteAfter = IERC20(USDC).balanceOf(POOL_CWUSDC_USDC); uint256 actualBaseOut = poolBaseBefore - poolBaseAfter; uint256 actualQuoteIntoPool = poolQuoteAfter - poolQuoteBefore; uint256 actualSurplus = receiverQuoteAfter - receiverQuoteBefore; uint256 premium = _aavePremium(amount); (uint256 predictedBaseOut, uint256 predictedUnwindOut, uint256 predictedSurplus) = _predictQuotePush(poolBaseBefore, poolQuoteBefore, amount, 3, 130, 100, bridgeAmount, premium); uint256 predictedNetQuoteIn = _netQuoteIn(amount, 3); assertGt(receiverQuoteAfter, receiverQuoteBefore, "receiver still retains quote surplus"); assertEq(cusdc138.balanceOf(destinationRecipient), destinationRecipientBefore + bridgeAmount, "destination funded"); assertEq(IERC20(CWUSDC).balanceOf(address(receiver)), 0, "remaining base fully unwound"); _assertWithinOnePercent(actualBaseOut, predictedBaseOut, "atomic baseOut"); _assertWithinOnePercent(actualQuoteIntoPool, predictedNetQuoteIn, "atomic netQuoteIn"); _assertWithinOnePercent(actualSurplus, predictedSurplus, "atomic surplus"); assertApproxEqAbs(predictedUnwindOut, actualSurplus + amount + premium, 2, "atomic unwindOut"); AtomicTypes.CorridorLiquidityState memory state = atomicVault.getCorridorLiquidityState(atomicCorridorId, address(cusdc138)); assertEq(state.settlementBacklog, bridgeAmount, "backlog increased by delivered amount"); assertEq(state.totalLiquidity, 5_000_000 - bridgeAmount, "vault liquidity debited on immediate fulfillment"); } function testFork_aaveQuotePush_atomicCorridorSettlementConfirmation_1_to_138() public { uint256 amount = 2_964_298; uint256 bridgeAmount = 500_000; uint256 deadline = block.timestamp + 1 hours; uint256 receiverBondBefore = atomicRegistry.availableBond(address(receiver)); uint256 treasuryBaseBefore = IERC20(CWUSDC).balanceOf(address(this)); uint256 receiverBaseBefore = IERC20(CWUSDC).balanceOf(address(receiver)); AaveQuotePushFlashReceiver.QuotePushParams memory p = AaveQuotePushFlashReceiver.QuotePushParams({ integration: DODO_PMM_INTEGRATION_MAINNET, pmmPool: POOL_CWUSDC_USDC, baseToken: CWUSDC, externalUnwinder: address(unwinder), minOutPmm: 2_800_000, minOutUnwind: amount + 1_483, unwindData: bytes(""), atomicBridge: AaveQuotePushFlashReceiver.AtomicBridgeParams({ coordinator: address(atomicCoordinator), sourceChain: 1, destinationChain: 138, destinationAsset: address(cusdc138), bridgeAmount: bridgeAmount, minDestinationAmount: bridgeAmount, destinationRecipient: destinationRecipient, destinationDeadline: deadline, routeId: atomicCorridorId, settlementMode: bytes32(0), submitCommitment: true }) }); receiver.flashQuotePush(USDC, amount, p); bytes32 obligationId = _deriveObligationId(bridgeAmount, deadline); AtomicTypes.AtomicObligation memory fulfilled = atomicCoordinator.getObligation(obligationId); assertEq(uint8(fulfilled.status), uint8(AtomicTypes.ObligationStatus.Fulfilled), "obligation fulfilled"); atomicCoordinator.initiateSettlement(obligationId, abi.encodePacked(bytes32(uint256(138)))); AtomicTypes.AtomicObligation memory pending = atomicCoordinator.getObligation(obligationId); assertEq(uint8(pending.status), uint8(AtomicTypes.ObligationStatus.SettlementPending), "obligation pending"); assertEq(mockSettlementAdapter.lastObligationId(), obligationId, "adapter saw obligation"); assertEq(mockSettlementAdapter.lastToken(), CWUSDC, "adapter token"); assertEq(mockSettlementAdapter.lastRecipient(), destinationRecipient, "adapter recipient"); uint256 expectedFulfillerFee = (bridgeAmount * 25) / 10_000; uint256 expectedProtocolFee = (bridgeAmount * 10) / 10_000; uint256 expectedSettlementAmount = bridgeAmount - expectedFulfillerFee - expectedProtocolFee; assertEq(mockSettlementAdapter.lastAmount(), expectedSettlementAmount, "adapter amount"); assertEq(IERC20(CWUSDC).balanceOf(address(this)), treasuryBaseBefore + expectedProtocolFee, "protocol fee received"); assertEq( IERC20(CWUSDC).balanceOf(address(receiver)), receiverBaseBefore + expectedFulfillerFee, "fulfiller fee received" ); cusdc138.mint(address(this), bridgeAmount); cusdc138.approve(address(atomicVault), bridgeAmount); atomicCoordinator.confirmSettlement(obligationId, bridgeAmount); AtomicTypes.AtomicObligation memory settled = atomicCoordinator.getObligation(obligationId); assertEq(uint8(settled.status), uint8(AtomicTypes.ObligationStatus.Settled), "obligation settled"); AtomicTypes.CorridorLiquidityState memory state = atomicVault.getCorridorLiquidityState(atomicCorridorId, address(cusdc138)); assertEq(state.settlementBacklog, 0, "backlog cleared"); assertEq(state.totalLiquidity, 5_000_000, "vault replenished"); assertEq(atomicRegistry.availableBond(address(receiver)), receiverBondBefore, "bond released"); } function _deriveObligationId(uint256 bridgeAmount, uint256 deadline) internal view returns (bytes32) { bytes32 intentId = keccak256( abi.encode( block.chainid, address(receiver), uint256(1), atomicCorridorId, bridgeAmount, bridgeAmount, deadline, atomicCorridorId ) ); return keccak256(abi.encode(intentId, destinationRecipient)); } function _predictQuotePush( uint256 baseReserve, uint256 quoteReserve, uint256 grossQuoteIn, uint256 lpFeeBps, uint256 unwindNumerator, uint256 unwindDenominator, uint256 bridgeAmount, uint256 premium ) internal pure returns (uint256 predictedBaseOut, uint256 predictedUnwindOut, uint256 predictedSurplus) { uint256 netQuoteIn = (grossQuoteIn * (10_000 - lpFeeBps)) / 10_000; predictedBaseOut = (netQuoteIn * baseReserve) / (quoteReserve + netQuoteIn); uint256 remainingBase = predictedBaseOut - bridgeAmount; predictedUnwindOut = (remainingBase * unwindNumerator) / unwindDenominator; predictedSurplus = predictedUnwindOut - grossQuoteIn - premium; } function _netQuoteIn(uint256 grossQuoteIn, uint256 lpFeeBps) internal pure returns (uint256) { return (grossQuoteIn * (10_000 - lpFeeBps)) / 10_000; } function _assertWithinOnePercent(uint256 actual, uint256 expected, string memory label) internal pure { if (expected == 0) { require(actual == 0, label); return; } uint256 diff = actual > expected ? actual - expected : expected - actual; require(diff * 10_000 <= expected * 100, label); } function _aavePremium(uint256 amount) internal pure returns (uint256) { return (amount * 5) / 10_000; } }