Skip to content

Commit

Permalink
test: show how approx dx can be quoted
Browse files Browse the repository at this point in the history
  • Loading branch information
danielattilasimon committed Feb 8, 2025
1 parent 7ed41cc commit 9f5ded7
Show file tree
Hide file tree
Showing 3 changed files with 182 additions and 1 deletion.
3 changes: 2 additions & 1 deletion contracts/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
173 changes: 173 additions & 0 deletions contracts/test/ExchangeHelpers.t.sol
Original file line number Diff line number Diff line change
@@ -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");
}
}
}
7 changes: 7 additions & 0 deletions contracts/test/Utils/UseDeployment.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -73,6 +74,7 @@ contract UseDeployment is CommonBase {
ICollateralRegistry collateralRegistry;
IBoldToken boldToken;
IHintHelpers hintHelpers;
IExchangeHelpers exchangeHelpers;
Governance governance;
ICurveStableSwapNG curveUsdcBold;
ILiquidityGaugeV6 curveUsdcBoldGauge;
Expand All @@ -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"));
Expand All @@ -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");
Expand Down

0 comments on commit 9f5ded7

Please sign in to comment.