From 9f5ded78cbe38d4665676540cacf1075f4d89012 Mon Sep 17 00:00:00 2001 From: Daniel Simon Date: Sat, 8 Feb 2025 11:53:23 +0700 Subject: [PATCH] test: show how approx dx can be quoted --- contracts/foundry.toml | 3 +- contracts/test/ExchangeHelpers.t.sol | 173 +++++++++++++++++++++++++ contracts/test/Utils/UseDeployment.sol | 7 + 3 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 contracts/test/ExchangeHelpers.t.sol diff --git a/contracts/foundry.toml b/contracts/foundry.toml index 964ec6816..950446267 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -9,7 +9,8 @@ ignored_error_codes = [3860, 5574] # contract-size fs_permissions = [ { access = "read", path = "./utils/assets/" }, { access = "read-write", path = "./utils/assets/test_output" }, - { access = "read-write", path = "./deployment-manifest.json" } + { access = "read-write", path = "./deployment-manifest.json" }, + { access = "read", path = "./addresses/" } ] [invariant] diff --git a/contracts/test/ExchangeHelpers.t.sol b/contracts/test/ExchangeHelpers.t.sol new file mode 100644 index 000000000..b74fe1bd4 --- /dev/null +++ b/contracts/test/ExchangeHelpers.t.sol @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {Test} from "forge-std/Test.sol"; +import {stdMath} from "forge-std/StdMath.sol"; +import {IERC20Metadata as IERC20} from "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {IQuoterV2} from "../src/Zappers/Modules/Exchanges/UniswapV3/IQuoterV2.sol"; +import {ISwapRouter} from "../src/Zappers/Modules/Exchanges/UniswapV3/ISwapRouter.sol"; +import {HybridCurveUniV3ExchangeHelpers} from "../src/Zappers/Modules/Exchanges/HybridCurveUniV3ExchangeHelpers.sol"; +import {UseDeployment} from "./Utils/UseDeployment.sol"; + +library Bytes { + function slice(bytes memory array, uint256 start) internal pure returns (bytes memory sliced) { + sliced = new bytes(array.length - start); + + for (uint256 i = 0; i < sliced.length; ++i) { + sliced[i] = array[start + i]; + } + } +} + +library BytesArray { + function clone(bytes[] memory array) internal pure returns (bytes[] memory cloned) { + cloned = new bytes[](array.length); + + for (uint256 i = 0; i < array.length; ++i) { + cloned[i] = array[i]; + } + } + + function reverse(bytes[] memory array) internal pure returns (bytes[] memory) { + for ((uint256 i, uint256 j) = (0, array.length - 1); i < j; (++i, --j)) { + (array[i], array[j]) = (array[j], array[i]); + } + + return array; + } + + function join(bytes[] memory array) internal pure returns (bytes memory joined) { + for (uint256 i = 0; i < array.length; ++i) { + joined = bytes.concat(joined, array[i]); + } + } +} + +contract ExchangeHelpersTest is Test, UseDeployment { + using Bytes for bytes; + using BytesArray for bytes[]; + + uint24 constant UNIV3_FEE_USDC_WETH = 500; // 0.05% + uint24 constant UNIV3_FEE_WETH_COLL = 100; // 0.01% + + IQuoterV2 constant uniV3Quoter = IQuoterV2(0x61fFE014bA17989E743c5F6cB21bF9697530B21e); + ISwapRouter constant uniV3Router = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); + + error QuoteResult(uint256 amount); + + function setUp() external { + string memory rpcUrl = vm.envOr("MAINNET_RPC_URL", string("")); + if (bytes(rpcUrl).length == 0) vm.skip(true); + + uint256 forkBlock = vm.envOr("FORK_BLOCK", uint256(0)); + if (forkBlock != 0) { + vm.createSelectFork(rpcUrl, forkBlock); + } else { + vm.createSelectFork(rpcUrl); + } + + _loadDeploymentFromManifest("addresses/1.json"); + } + + function test_Curve_CanQuoteApproxDx(bool zeroToOne, uint256 dyExpected) external { + (int128 i, int128 j) = zeroToOne ? (int128(0), int128(1)) : (int128(1), int128(0)); + (address inputToken, address outputToken) = (curveUsdcBold.coins(uint128(i)), curveUsdcBold.coins(uint128(j))); + uint256 dyDecimals = IERC20(outputToken).decimals(); + uint256 dyDiv = 10 ** (18 - dyDecimals); + dyExpected = bound(dyExpected, 1, 1_000_000 ether / dyDiv); + + uint256 dx = curveUsdcBold.get_dx(i, j, dyExpected); + vm.assume(dx > 0); // Curve reverts in this case + + uint256 balance0 = IERC20(outputToken).balanceOf(address(this)); + deal(inputToken, address(this), dx); + IERC20(inputToken).approve(address(curveUsdcBold), dx); + uint256 dy = curveUsdcBold.exchange(i, j, dx, 0); + + assertEqDecimal(IERC20(outputToken).balanceOf(address(this)) - balance0, dy, dyDecimals, "balance != dy"); + assertApproxEq(dy, dyExpected, 2e-6 ether / dyDiv, 3e-6 ether, dyDecimals, "dy !~= expected dy"); + } + + function test_UniV3_CanQuoteApproxDx(bool collToUsdc, uint256 collIndex, uint256 dyExpected) external { + collIndex = bound(collIndex, 0, branches.length - 1); + address collToken = address(branches[collIndex].collToken); + (address inputToken, address outputToken) = collToUsdc ? (collToken, USDC) : (USDC, collToken); + uint256 dyDecimals = IERC20(outputToken).decimals(); + uint256 dyDiv = 10 ** (18 - dyDecimals); + dyExpected = bound(dyExpected, 1, (collToUsdc ? 100_000 ether : 100 ether) / dyDiv); + + bytes[] memory pathUsdcToColl = new bytes[](collToken == WETH ? 3 : 5); + pathUsdcToColl[0] = abi.encodePacked(USDC); + pathUsdcToColl[1] = abi.encodePacked(UNIV3_FEE_USDC_WETH); + pathUsdcToColl[2] = abi.encodePacked(WETH); + if (collToken != WETH) { + pathUsdcToColl[3] = abi.encodePacked(UNIV3_FEE_WETH_COLL); + pathUsdcToColl[4] = abi.encodePacked(collToken); + } + + bytes[] memory pathCollToUsdc = pathUsdcToColl.clone().reverse(); + (bytes memory swapPath, bytes memory quotePath) = + collToUsdc ? (pathCollToUsdc.join(), pathUsdcToColl.join()) : (pathUsdcToColl.join(), pathCollToUsdc.join()); + + uint256 dx = uniV3Quoter_quoteExactOutput(quotePath, dyExpected); + // vm.assume(dx > 0); // Fine by Uniswap + + uint256 balance0 = IERC20(outputToken).balanceOf(address(this)); + deal(inputToken, address(this), dx); + IERC20(inputToken).approve(address(uniV3Router), dx); + uint256 dy = uniV3Router.exactInput( + ISwapRouter.ExactInputParams({ + path: swapPath, + recipient: address(this), + deadline: block.timestamp, + amountIn: dx, + amountOutMinimum: 0 + }) + ); + + assertEqDecimal(IERC20(outputToken).balanceOf(address(this)) - balance0, dy, dyDecimals, "balance != dy"); + assertApproxEqAbsDecimal(dy, dyExpected, 4e-10 ether / dyDiv, dyDecimals, "dy !~= expected dy"); + } + + function uniV3Quoter_throw_quoteExactOutput(bytes memory path, uint256 amountOut) external { + (uint256 amountIn,,,) = uniV3Quoter.quoteExactOutput(path, amountOut); + revert QuoteResult(amountIn); + } + + function _revert(bytes memory revertData) internal pure { + assembly { + revert(add(32, revertData), mload(revertData)) + } + } + + function uniV3Quoter_quoteExactOutput(bytes memory path, uint256 amountOut) internal returns (uint256 amountIn) { + try this.uniV3Quoter_throw_quoteExactOutput(path, amountOut) { + revert("Should have reverted"); + } catch (bytes memory revertData) { + bytes4 selector = bytes4(revertData); + if (selector == QuoteResult.selector && revertData.length == 4 + 32) { + amountIn = uint256(bytes32(revertData.slice(4))); + } else { + _revert(revertData); // bubble + } + } + } + + function assertApproxEq(uint256 a, uint256 b, uint256 maxAbs, uint256 maxRel, uint256 decimals, string memory err) + internal + pure + { + uint256 abs = stdMath.delta(a, b); + uint256 rel = stdMath.percentDelta(a, b); + + if (abs > maxAbs && rel > maxRel) { + if (rel > maxRel) { + assertApproxEqRelDecimal(a, b, maxRel, decimals, err); + } else { + assertApproxEqAbsDecimal(a, b, maxAbs, decimals, err); + } + + revert("Assertion should have failed"); + } + } +} diff --git a/contracts/test/Utils/UseDeployment.sol b/contracts/test/Utils/UseDeployment.sol index 127e14a29..6baeccc8e 100644 --- a/contracts/test/Utils/UseDeployment.sol +++ b/contracts/test/Utils/UseDeployment.sol @@ -8,6 +8,7 @@ import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; import {IUserProxy} from "V2-gov/src/interfaces/IUserProxy.sol"; import {CurveV2GaugeRewards} from "V2-gov/src/CurveV2GaugeRewards.sol"; import {Governance} from "V2-gov/src/Governance.sol"; +import {IExchangeHelpers} from "src/Zappers/Interfaces/IExchangeHelpers.sol"; import {ILeverageZapper} from "src/Zappers/Interfaces/ILeverageZapper.sol"; import {IZapper} from "src/Zappers/Interfaces/IZapper.sol"; import {IActivePool} from "src/Interfaces/IActivePool.sol"; @@ -73,6 +74,7 @@ contract UseDeployment is CommonBase { ICollateralRegistry collateralRegistry; IBoldToken boldToken; IHintHelpers hintHelpers; + IExchangeHelpers exchangeHelpers; Governance governance; ICurveStableSwapNG curveUsdcBold; ILiquidityGaugeV6 curveUsdcBoldGauge; @@ -88,6 +90,7 @@ contract UseDeployment is CommonBase { collateralRegistry = ICollateralRegistry(json.readAddress(".collateralRegistry")); boldToken = IBoldToken(BOLD = json.readAddress(".boldToken")); hintHelpers = IHintHelpers(json.readAddress(".hintHelpers")); + exchangeHelpers = IExchangeHelpers(json.readAddress(".exchangeHelpers")); governance = Governance(json.readAddress(".governance.governance")); curveUsdcBold = ICurveStableSwapNG(json.readAddress(".governance.curveUsdcBoldPool")); curveUsdcBoldGauge = ILiquidityGaugeV6(json.readAddress(".governance.curveUsdcBoldGauge")); @@ -99,10 +102,14 @@ contract UseDeployment is CommonBase { vm.label(address(collateralRegistry), "CollateralRegistry"); vm.label(address(hintHelpers), "HintHelpers"); + vm.label(address(exchangeHelpers), "ExchangeHelpers"); vm.label(address(governance), "Governance"); vm.label(address(curveUsdcBold), "CurveStableSwapNG"); vm.label(address(curveUsdcBoldGauge), "LiquidityGaugeV6"); vm.label(address(curveUsdcBoldInitiative), "CurveV2GaugeRewards"); + vm.label(address(curveLusdBold), "CurveStableSwapNG"); + vm.label(address(curveLusdBoldGauge), "LiquidityGaugeV6"); + vm.label(address(curveLusdBoldInitiative), "CurveV2GaugeRewards"); ETH_GAS_COMPENSATION = json.readUint(".constants.ETH_GAS_COMPENSATION"); MIN_DEBT = json.readUint(".constants.MIN_DEBT");