// 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 { AaveUniV2CwStableRebalanceFlashReceiver } from "../../contracts/flash/AaveUniV2CwStableRebalanceFlashReceiver.sol"; contract AaveUniV2CwStableRebalanceFlashReceiverMainnetForkTest is Test { address constant AAVE_POOL = 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2; address constant PAIR = 0xC28706F899266b36BC43cc072b3a921BDf2C48D9; address constant ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; address constant CWUSDC = 0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a; address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; address constant TOKEN_JAR = 0xf38521f130fcCF29dB1961597bc5d2B60F995f85; bool internal forkAvailable; AaveUniV2CwStableRebalanceFlashReceiver internal receiver; modifier skipIfNoFork() { if (!forkAvailable) return; _; } function setUp() public { string memory rpcUrl = vm.envOr("ETHEREUM_MAINNET_RPC", string("")); if (bytes(rpcUrl).length == 0) { forkAvailable = false; return; } try vm.createSelectFork(rpcUrl) { forkAvailable = true; } catch { forkAvailable = false; return; } receiver = new AaveUniV2CwStableRebalanceFlashReceiver(AAVE_POOL, address(this)); } function testFork_flashRebalanceRemove_withTokenJarLp() public skipIfNoFork { uint256 lpBal = IERC20(PAIR).balanceOf(TOKEN_JAR); if (lpBal == 0) { return; } (uint112 r0, uint112 r1,) = _reserves(); uint256 baseRaw = address(IERC20(CWUSDC)) < address(IERC20(USDC)) ? r0 : r1; uint256 quoteRaw = address(IERC20(CWUSDC)) < address(IERC20(USDC)) ? r1 : r0; if (quoteRaw == 0 || baseRaw <= quoteRaw) { return; } uint256 flashIn = _quoteInToEqualize(baseRaw, quoteRaw); uint256 premium = (flashIn * 5 + 9999) / 10000; uint256 lpUse = lpBal; vm.prank(TOKEN_JAR); IERC20(PAIR).transfer(address(receiver), lpUse); uint256 minCwRebalance = 1; uint256 minCwRemove = 1; uint256 minStableRemove = 1; uint256 cwToSell = flashIn + premium + 50_000; uint256 minStableRepay = flashIn + premium; address recipient = address(0xBEEF); receiver.runRebalanceRemove( USDC, flashIn, AaveUniV2CwStableRebalanceFlashReceiver.RebalanceRemoveParams({ router: ROUTER, pair: PAIR, cwToken: CWUSDC, stableToken: USDC, lpAmount: lpUse, rebalanceStableIn: flashIn, minCwFromRebalance: minCwRebalance, minStableFromRemove: minStableRemove, minCwFromRemove: minCwRemove, cwToSellForRepay: cwToSell, minStableFromRepaySwap: minStableRepay, recipient: recipient }) ); assertEq(IERC20(PAIR).balanceOf(address(receiver)), 0, "LP consumed"); assertGt(IERC20(USDC).balanceOf(recipient) + IERC20(CWUSDC).balanceOf(recipient), 0, "recipient funded"); } function _reserves() internal view returns (uint112 r0, uint112 r1, uint32 ts) { (r0, r1, ts) = _getReserves(PAIR); } function _getReserves(address pair) internal view returns (uint112, uint112, uint32) { (bool ok, bytes memory data) = pair.staticcall(abi.encodeWithSignature("getReserves()")); require(ok, "getReserves failed"); return abi.decode(data, (uint112, uint112, uint32)); } function _quoteInToEqualize(uint256 baseRaw, uint256 quoteRaw) internal pure returns (uint256) { uint256 y = quoteRaw; uint256 x = baseRaw; // Integer approximation of closed-form quote-in (matches planner within rounding). uint256 xy = x * y; uint256 target = _isqrt(xy); if (target <= y) return y + 1; uint256 need = target - y; return (need * 1005) / 997 + 1; } function _isqrt(uint256 n) internal pure returns (uint256) { if (n == 0) return 0; uint256 x = n; uint256 z = (x + 1) / 2; while (z < x) { x = z; z = (x + n / x) / 2; } return x; } }