|
| 1 | +// SPDX-License-Identifier: MIT |
| 2 | +pragma solidity 0.8.24; |
| 3 | + |
| 4 | +import {Test} from "forge-std/Test.sol"; |
| 5 | +import {stdMath} from "forge-std/StdMath.sol"; |
| 6 | +import {IERC20Metadata as IERC20} from "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol"; |
| 7 | +import {IQuoterV2} from "../src/Zappers/Modules/Exchanges/UniswapV3/IQuoterV2.sol"; |
| 8 | +import {ISwapRouter} from "../src/Zappers/Modules/Exchanges/UniswapV3/ISwapRouter.sol"; |
| 9 | +import {HybridCurveUniV3ExchangeHelpers} from "../src/Zappers/Modules/Exchanges/HybridCurveUniV3ExchangeHelpers.sol"; |
| 10 | +import {UseDeployment} from "./Utils/UseDeployment.sol"; |
| 11 | + |
| 12 | +library Bytes { |
| 13 | + function slice(bytes memory array, uint256 start) internal pure returns (bytes memory sliced) { |
| 14 | + sliced = new bytes(array.length - start); |
| 15 | + |
| 16 | + for (uint256 i = 0; i < sliced.length; ++i) { |
| 17 | + sliced[i] = array[start + i]; |
| 18 | + } |
| 19 | + } |
| 20 | +} |
| 21 | + |
| 22 | +library BytesArray { |
| 23 | + function clone(bytes[] memory array) internal pure returns (bytes[] memory cloned) { |
| 24 | + cloned = new bytes[](array.length); |
| 25 | + |
| 26 | + for (uint256 i = 0; i < array.length; ++i) { |
| 27 | + cloned[i] = array[i]; |
| 28 | + } |
| 29 | + } |
| 30 | + |
| 31 | + function reverse(bytes[] memory array) internal pure returns (bytes[] memory) { |
| 32 | + for ((uint256 i, uint256 j) = (0, array.length - 1); i < j; (++i, --j)) { |
| 33 | + (array[i], array[j]) = (array[j], array[i]); |
| 34 | + } |
| 35 | + |
| 36 | + return array; |
| 37 | + } |
| 38 | + |
| 39 | + function join(bytes[] memory array) internal pure returns (bytes memory joined) { |
| 40 | + for (uint256 i = 0; i < array.length; ++i) { |
| 41 | + joined = bytes.concat(joined, array[i]); |
| 42 | + } |
| 43 | + } |
| 44 | +} |
| 45 | + |
| 46 | +contract ExchangeHelpersTest is Test, UseDeployment { |
| 47 | + using Bytes for bytes; |
| 48 | + using BytesArray for bytes[]; |
| 49 | + |
| 50 | + uint24 constant UNIV3_FEE_USDC_WETH = 500; // 0.05% |
| 51 | + uint24 constant UNIV3_FEE_WETH_COLL = 100; // 0.01% |
| 52 | + |
| 53 | + IQuoterV2 constant uniV3Quoter = IQuoterV2(0x61fFE014bA17989E743c5F6cB21bF9697530B21e); |
| 54 | + ISwapRouter constant uniV3Router = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); |
| 55 | + |
| 56 | + error QuoteResult(uint256 amount); |
| 57 | + |
| 58 | + function setUp() external { |
| 59 | + string memory rpcUrl = vm.envOr("MAINNET_RPC_URL", string("")); |
| 60 | + if (bytes(rpcUrl).length == 0) vm.skip(true); |
| 61 | + |
| 62 | + uint256 forkBlock = vm.envOr("FORK_BLOCK", uint256(0)); |
| 63 | + if (forkBlock != 0) { |
| 64 | + vm.createSelectFork(rpcUrl, forkBlock); |
| 65 | + } else { |
| 66 | + vm.createSelectFork(rpcUrl); |
| 67 | + } |
| 68 | + |
| 69 | + _loadDeploymentFromManifest("addresses/1.json"); |
| 70 | + } |
| 71 | + |
| 72 | + function test_Curve_CanQuoteApproxDx(bool zeroToOne, uint256 dyExpected) external { |
| 73 | + (int128 i, int128 j) = zeroToOne ? (int128(0), int128(1)) : (int128(1), int128(0)); |
| 74 | + (address inputToken, address outputToken) = (curveUsdcBold.coins(uint128(i)), curveUsdcBold.coins(uint128(j))); |
| 75 | + uint256 dyDecimals = IERC20(outputToken).decimals(); |
| 76 | + uint256 dyDiv = 10 ** (18 - dyDecimals); |
| 77 | + dyExpected = bound(dyExpected, 1, 1_000_000 ether / dyDiv); |
| 78 | + |
| 79 | + uint256 dx = curveUsdcBold.get_dx(i, j, dyExpected); |
| 80 | + vm.assume(dx > 0); // Curve reverts in this case |
| 81 | + |
| 82 | + uint256 balance0 = IERC20(outputToken).balanceOf(address(this)); |
| 83 | + deal(inputToken, address(this), dx); |
| 84 | + IERC20(inputToken).approve(address(curveUsdcBold), dx); |
| 85 | + uint256 dy = curveUsdcBold.exchange(i, j, dx, 0); |
| 86 | + |
| 87 | + assertEqDecimal(IERC20(outputToken).balanceOf(address(this)) - balance0, dy, dyDecimals, "balance != dy"); |
| 88 | + assertApproxEqAbsRelDecimal(dy, dyExpected, 2e-6 ether / dyDiv, 1e-5 ether, dyDecimals, "dy !~= expected dy"); |
| 89 | + } |
| 90 | + |
| 91 | + function test_UniV3_CanQuoteApproxDx(bool collToUsdc, uint256 collIndex, uint256 dyExpected) external { |
| 92 | + collIndex = bound(collIndex, 0, branches.length - 1); |
| 93 | + address collToken = address(branches[collIndex].collToken); |
| 94 | + (address inputToken, address outputToken) = collToUsdc ? (collToken, USDC) : (USDC, collToken); |
| 95 | + uint256 dyDecimals = IERC20(outputToken).decimals(); |
| 96 | + uint256 dyDiv = 10 ** (18 - dyDecimals); |
| 97 | + dyExpected = bound(dyExpected, 1, (collToUsdc ? 100_000 ether : 100 ether) / dyDiv); |
| 98 | + |
| 99 | + bytes[] memory pathUsdcToColl = new bytes[](collToken == WETH ? 3 : 5); |
| 100 | + pathUsdcToColl[0] = abi.encodePacked(USDC); |
| 101 | + pathUsdcToColl[1] = abi.encodePacked(UNIV3_FEE_USDC_WETH); |
| 102 | + pathUsdcToColl[2] = abi.encodePacked(WETH); |
| 103 | + if (collToken != WETH) { |
| 104 | + pathUsdcToColl[3] = abi.encodePacked(UNIV3_FEE_WETH_COLL); |
| 105 | + pathUsdcToColl[4] = abi.encodePacked(collToken); |
| 106 | + } |
| 107 | + |
| 108 | + bytes[] memory pathCollToUsdc = pathUsdcToColl.clone().reverse(); |
| 109 | + (bytes memory swapPath, bytes memory quotePath) = |
| 110 | + collToUsdc ? (pathCollToUsdc.join(), pathUsdcToColl.join()) : (pathUsdcToColl.join(), pathCollToUsdc.join()); |
| 111 | + |
| 112 | + uint256 dx = uniV3Quoter_quoteExactOutput(quotePath, dyExpected); |
| 113 | + // vm.assume(dx > 0); // Fine by Uniswap |
| 114 | + |
| 115 | + uint256 balance0 = IERC20(outputToken).balanceOf(address(this)); |
| 116 | + deal(inputToken, address(this), dx); |
| 117 | + IERC20(inputToken).approve(address(uniV3Router), dx); |
| 118 | + uint256 dy = uniV3Router.exactInput( |
| 119 | + ISwapRouter.ExactInputParams({ |
| 120 | + path: swapPath, |
| 121 | + recipient: address(this), |
| 122 | + deadline: block.timestamp, |
| 123 | + amountIn: dx, |
| 124 | + amountOutMinimum: 0 |
| 125 | + }) |
| 126 | + ); |
| 127 | + |
| 128 | + assertEqDecimal(IERC20(outputToken).balanceOf(address(this)) - balance0, dy, dyDecimals, "balance != dy"); |
| 129 | + assertApproxEqAbsDecimal(dy, dyExpected, 4e-10 ether / dyDiv, dyDecimals, "dy !~= expected dy"); |
| 130 | + } |
| 131 | + |
| 132 | + function uniV3Quoter_throw_quoteExactOutput(bytes memory path, uint256 amountOut) external { |
| 133 | + (uint256 amountIn,,,) = uniV3Quoter.quoteExactOutput(path, amountOut); |
| 134 | + revert QuoteResult(amountIn); |
| 135 | + } |
| 136 | + |
| 137 | + function _revert(bytes memory revertData) internal pure { |
| 138 | + assembly { |
| 139 | + revert(add(32, revertData), mload(revertData)) |
| 140 | + } |
| 141 | + } |
| 142 | + |
| 143 | + function uniV3Quoter_quoteExactOutput(bytes memory path, uint256 amountOut) internal returns (uint256 amountIn) { |
| 144 | + try this.uniV3Quoter_throw_quoteExactOutput(path, amountOut) { |
| 145 | + revert("Should have reverted"); |
| 146 | + } catch (bytes memory revertData) { |
| 147 | + bytes4 selector = bytes4(revertData); |
| 148 | + if (selector == QuoteResult.selector && revertData.length == 4 + 32) { |
| 149 | + amountIn = uint256(bytes32(revertData.slice(4))); |
| 150 | + } else { |
| 151 | + _revert(revertData); // bubble |
| 152 | + } |
| 153 | + } |
| 154 | + } |
| 155 | + |
| 156 | + function assertApproxEqAbsRelDecimal( |
| 157 | + uint256 a, |
| 158 | + uint256 b, |
| 159 | + uint256 maxAbs, |
| 160 | + uint256 maxRel, |
| 161 | + uint256 decimals, |
| 162 | + string memory err |
| 163 | + ) internal pure { |
| 164 | + uint256 abs = stdMath.delta(a, b); |
| 165 | + uint256 rel = stdMath.percentDelta(a, b); |
| 166 | + |
| 167 | + if (abs > maxAbs && rel > maxRel) { |
| 168 | + if (rel > maxRel) { |
| 169 | + assertApproxEqRelDecimal(a, b, maxRel, decimals, err); |
| 170 | + } else { |
| 171 | + assertApproxEqAbsDecimal(a, b, maxAbs, decimals, err); |
| 172 | + } |
| 173 | + |
| 174 | + revert("Assertion should have failed"); |
| 175 | + } |
| 176 | + } |
| 177 | +} |
0 commit comments