Skip to content

Commit

Permalink
test: exchange helpers V2
Browse files Browse the repository at this point in the history
  • Loading branch information
danielattilasimon committed Feb 8, 2025
1 parent 411d1b7 commit 628c4a2
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 60 deletions.
6 changes: 2 additions & 4 deletions contracts/src/Zappers/Interfaces/IExchangeHelpersV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@

pragma solidity ^0.8.0;

import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";

interface IExchangeHelpersV2 {
function getDy(uint256 _dx, bool _collToBold, IERC20 _collToken) external returns (uint256 dy);
function getDx(uint256 _dy, bool _collToBold, IERC20 _collToken) external returns (uint256 dx);
function getDy(uint256 _dx, bool _collToBold, address _collToken) external returns (uint256 dy);
function getDx(uint256 _dy, bool _collToBold, address _collToken) external returns (uint256 dx);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,31 @@

pragma solidity ^0.8.18;

import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {IWETH} from "../../../Interfaces/IWETH.sol";
import {ICurveStableswapNGPool} from "./Curve/ICurveStableswapNGPool.sol";
import {IQuoterV2} from "./UniswapV3/IQuoterV2.sol";
import {IExchangeHelpersV2} from "../../Interfaces/IExchangeHelpersV2.sol";

contract HybridCurveUniV3ExchangeHelpersV2 is IExchangeHelpersV2 {
IERC20 public immutable USDC;
IWETH public immutable WETH;
address public immutable USDC;
address public immutable WETH;

// Curve
ICurveStableswapNGPool public immutable curvePool;
uint128 public immutable USDC_INDEX;
uint128 public immutable BOLD_TOKEN_INDEX;
int128 public immutable USDC_INDEX;
int128 public immutable BOLD_TOKEN_INDEX;

// Uniswap
uint24 public immutable feeUsdcWeth;
uint24 public immutable feeWethColl;
IQuoterV2 public immutable uniV3Quoter;

constructor(
IERC20 _usdc,
IWETH _weth,
address _usdc,
address _weth,
// Curve
ICurveStableswapNGPool _curvePool,
uint128 _usdcIndex,
uint128 _boldIndex,
int128 _usdcIndex,
int128 _boldIndex,
// UniV3
uint24 _feeUsdcWeth,
uint24 _feeWethColl,
Expand All @@ -48,11 +46,11 @@ contract HybridCurveUniV3ExchangeHelpersV2 is IExchangeHelpersV2 {
uniV3Quoter = _uniV3Quoter;
}

function getDy(uint256 _dx, bool _collToBold, IERC20 _collToken) external returns (uint256 dy) {
function getDy(uint256 _dx, bool _collToBold, address _collToken) external returns (uint256 dy) {
if (_collToBold) {
// (Coll ->) WETH -> USDC?
bytes memory path;
if (address(WETH) == address(_collToken)) {
if (WETH == _collToken) {
path = abi.encodePacked(WETH, feeUsdcWeth, USDC);
} else {
path = abi.encodePacked(_collToken, feeWethColl, WETH, feeUsdcWeth, USDC);
Expand All @@ -61,14 +59,14 @@ contract HybridCurveUniV3ExchangeHelpersV2 is IExchangeHelpersV2 {
(uint256 uniDy,,,) = uniV3Quoter.quoteExactInput(path, _dx);

// USDC -> BOLD?
dy = curvePool.get_dy(int128(USDC_INDEX), int128(BOLD_TOKEN_INDEX), uniDy);
dy = curvePool.get_dy(USDC_INDEX, BOLD_TOKEN_INDEX, uniDy);
} else {
// BOLD -> USDC?
uint256 curveDy = curvePool.get_dy(int128(BOLD_TOKEN_INDEX), int128(USDC_INDEX), _dx);
uint256 curveDy = curvePool.get_dy(BOLD_TOKEN_INDEX, USDC_INDEX, _dx);

// USDC -> WETH (-> Coll)?
bytes memory path;
if (address(WETH) == address(_collToken)) {
if (WETH == _collToken) {
path = abi.encodePacked(USDC, feeUsdcWeth, WETH);
} else {
path = abi.encodePacked(USDC, feeUsdcWeth, WETH, feeWethColl, _collToken);
Expand All @@ -78,15 +76,15 @@ contract HybridCurveUniV3ExchangeHelpersV2 is IExchangeHelpersV2 {
}
}

function getDx(uint256 _dy, bool _collToBold, IERC20 _collToken) external returns (uint256 dx) {
function getDx(uint256 _dy, bool _collToBold, address _collToken) external returns (uint256 dx) {
if (_collToBold) {
// USDC? -> BOLD
uint256 curveDx = curvePool.get_dx(int128(USDC_INDEX), int128(BOLD_TOKEN_INDEX), _dy);
uint256 curveDx = curvePool.get_dx(USDC_INDEX, BOLD_TOKEN_INDEX, _dy);

// Uniswap expects path to be reversed when quoting exact output
// USDC <- WETH (<- Coll)?
bytes memory path;
if (address(WETH) == address(_collToken)) {
if (WETH == _collToken) {
path = abi.encodePacked(USDC, feeUsdcWeth, WETH);
} else {
path = abi.encodePacked(USDC, feeUsdcWeth, WETH, feeWethColl, _collToken);
Expand All @@ -97,7 +95,7 @@ contract HybridCurveUniV3ExchangeHelpersV2 is IExchangeHelpersV2 {
// Uniswap expects path to be reversed when quoting exact output
// (Coll <-) WETH <- USDC?
bytes memory path;
if (address(WETH) == address(_collToken)) {
if (WETH == _collToken) {
path = abi.encodePacked(WETH, feeUsdcWeth, USDC);
} else {
path = abi.encodePacked(_collToken, feeWethColl, WETH, feeUsdcWeth, USDC);
Expand All @@ -106,7 +104,7 @@ contract HybridCurveUniV3ExchangeHelpersV2 is IExchangeHelpersV2 {
(uint256 uniDx,,,) = uniV3Quoter.quoteExactOutput(path, _dy);

// BOLD? -> USDC
dx = curvePool.get_dx(int128(BOLD_TOKEN_INDEX), int128(USDC_INDEX), uniDx);
dx = curvePool.get_dx(BOLD_TOKEN_INDEX, USDC_INDEX, uniDx);
}
}
}
178 changes: 142 additions & 36 deletions contracts/test/ExchangeHelpers.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ 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 {ICurveStableswapNGPool} from "../src/Zappers/Modules/Exchanges/Curve/ICurveStableswapNGPool.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 {HybridCurveUniV3ExchangeHelpersV2} from "../src/Zappers/Modules/Exchanges/HybridCurveUniV3ExchangeHelpersV2.sol";
import {IExchange} from "../src/Zappers/Interfaces/IExchange.sol";
import {IExchangeHelpersV2} from "../src/Zappers/Interfaces/IExchangeHelpersV2.sol";
import {UseDeployment} from "./Utils/UseDeployment.sol";

library Bytes {
Expand Down Expand Up @@ -53,6 +56,9 @@ contract ExchangeHelpersTest is Test, UseDeployment {
IQuoterV2 constant uniV3Quoter = IQuoterV2(0x61fFE014bA17989E743c5F6cB21bF9697530B21e);
ISwapRouter constant uniV3Router = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564);

mapping(address collToken => IExchange) exchange;
IExchangeHelpersV2 exchangeHelpersV2;

error QuoteResult(uint256 amount);

function setUp() external {
Expand All @@ -67,25 +73,41 @@ contract ExchangeHelpersTest is Test, UseDeployment {
}

_loadDeploymentFromManifest("addresses/1.json");

for (uint256 i = 0; i < branches.length; ++i) {
address collToken = address(branches[i].collToken);
exchange[collToken] = branches[i].zapper.exchange();
}

exchangeHelpersV2 = new HybridCurveUniV3ExchangeHelpersV2({
_usdc: USDC,
_weth: WETH,
_curvePool: ICurveStableswapNGPool(address(curveUsdcBold)),
_usdcIndex: int8(curveUsdcBold.coins(0) == USDC ? 0 : 1),
_boldIndex: int8(curveUsdcBold.coins(0) == BOLD ? 0 : 1),
_feeUsdcWeth: UNIV3_FEE_USDC_WETH,
_feeWethColl: UNIV3_FEE_WETH_COLL,
_uniV3Quoter: uniV3Quoter
});
}

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);
dyExpected = bound(dyExpected, 1 ether / dyDiv, 1_000_000 ether / dyDiv);

uint256 dx = curveUsdcBold.get_dx(i, j, dyExpected);
vm.assume(dx > 0); // Curve reverts in this case
vm.assume(dx > 0); // For some reason Curve sometimes says you can get >0 output tokens in exchange for 0 input

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");
assertApproxEqAbsRelDecimal(dy, dyExpected, 2e-6 ether / dyDiv, 1e-5 ether, dyDecimals, "dy !~= expected dy");
assertApproxEqRelDecimal(dy, dyExpected, 1e-5 ether, dyDecimals, "dy !~= expected dy");
}

function test_UniV3_CanQuoteApproxDx(bool collToUsdc, uint256 collIndex, uint256 dyExpected) external {
Expand All @@ -94,7 +116,12 @@ contract ExchangeHelpersTest is Test, UseDeployment {
(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);

if (collToUsdc) {
dyExpected = bound(dyExpected, 1 ether / dyDiv, 100_000 ether / dyDiv);
} else {
dyExpected = bound(dyExpected, 0.001 ether / dyDiv, 100 ether / dyDiv);
}

bytes[] memory pathUsdcToColl = new bytes[](collToken == WETH ? 3 : 5);
pathUsdcToColl[0] = abi.encodePacked(USDC);
Expand All @@ -110,11 +137,10 @@ contract ExchangeHelpersTest is Test, UseDeployment {
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,
Expand All @@ -129,49 +155,129 @@ contract ExchangeHelpersTest is Test, UseDeployment {
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 test_ExchangeHelpersV2_CanQuoteApproxDx(bool collToBold, uint256 collIndex, uint256 dyExpected) external {
collIndex = bound(collIndex, 0, branches.length - 1);
address collToken = address(branches[collIndex].collToken);
(address inputToken, address outputToken) = collToBold ? (collToken, BOLD) : (BOLD, collToken);

if (collToBold) {
dyExpected = bound(dyExpected, 1 ether, 100_000 ether);
} else {
dyExpected = bound(dyExpected, 0.001 ether, 100 ether);
}

uint256 dx = exchangeHelpersV2_getDx(dyExpected, collToBold, collToken);
uint256 balance0 = IERC20(outputToken).balanceOf(address(this));
deal(inputToken, address(this), dx);
IERC20(inputToken).approve(address(exchange[collToken]), dx);

if (collToBold) {
exchange[collToken].swapToBold(dx, 0);
} else {
exchange[collToken].swapFromBold(dx, 0);
}

uint256 dy = IERC20(outputToken).balanceOf(address(this)) - balance0;
assertApproxEqRelDecimal(dy, dyExpected, 1e-5 ether, 18, "dy !~= expected dy");
}

function _revert(bytes memory revertData) internal pure {
function test_ExchangeHelpersV2_CanQuoteExactDy(bool collToBold, uint256 collIndex, uint256 dx) external {
collIndex = bound(collIndex, 0, branches.length - 1);
address collToken = address(branches[collIndex].collToken);
(address inputToken, address outputToken) = collToBold ? (collToken, BOLD) : (BOLD, collToken);

if (collToBold) {
dx = bound(dx, 0.001 ether, 100 ether);
} else {
dx = bound(dx, 1 ether, 100_000 ether);
}

uint256 dyExpected = exchangeHelpersV2_getDy(dx, collToBold, collToken);
uint256 balance0 = IERC20(outputToken).balanceOf(address(this));
deal(inputToken, address(this), dx);
IERC20(inputToken).approve(address(exchange[collToken]), dx);

if (collToBold) {
exchange[collToken].swapToBold(dx, 0);
} else {
exchange[collToken].swapFromBold(dx, 0);
}

uint256 dy = IERC20(outputToken).balanceOf(address(this)) - balance0;
assertEqDecimal(dy, dyExpected, 18, "dy != expected dy");
}

function _revert(bytes memory revertData) private 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) {
function _decodeQuoteResult(bytes memory revertData) private pure returns (uint256) {
bytes4 selector = bytes4(revertData);
if (selector == QuoteResult.selector && revertData.length == 4 + 32) {
return uint256(bytes32(revertData.slice(4)));
} else {
_revert(revertData); // bubble
}
}

function uniV3Quoter_quoteExactOutput_throw(bytes memory path, uint256 amountOut) external {
(uint256 amountIn,,,) = uniV3Quoter.quoteExactOutput(path, amountOut);
revert QuoteResult(amountIn);
}

function uniV3Quoter_quoteExactOutput(bytes memory path, uint256 amountOut) internal returns (uint256) {
try this.uniV3Quoter_quoteExactOutput_throw(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
}
return _decodeQuoteResult(revertData);
}
}

function assertApproxEqAbsRelDecimal(
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);
function exchangeHelpersV2_getDx_throw(uint256 dy, bool collToBold, address collToken) external {
revert QuoteResult(exchangeHelpersV2.getDx(dy, collToBold, collToken));
}

function exchangeHelpersV2_getDx(uint256 dy, bool collToBold, address collToken) internal returns (uint256) {
try this.exchangeHelpersV2_getDx_throw(dy, collToBold, collToken) {
revert("Should have reverted");
} catch (bytes memory revertData) {
return _decodeQuoteResult(revertData);
}
}

if (abs > maxAbs && rel > maxRel) {
if (rel > maxRel) {
assertApproxEqRelDecimal(a, b, maxRel, decimals, err);
} else {
assertApproxEqAbsDecimal(a, b, maxAbs, decimals, err);
}
function exchangeHelpersV2_getDy_throw(uint256 dx, bool collToBold, address collToken) external {
revert QuoteResult(exchangeHelpersV2.getDy(dx, collToBold, collToken));
}

revert("Assertion should have failed");
function exchangeHelpersV2_getDy(uint256 dx, bool collToBold, address collToken) internal returns (uint256) {
try this.exchangeHelpersV2_getDy_throw(dx, collToBold, collToken) {
revert("Should have reverted");
} catch (bytes memory revertData) {
return _decodeQuoteResult(revertData);
}
}

// function assertApproxEqAbsRelDecimal(
// 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");
// }
// }
}

0 comments on commit 628c4a2

Please sign in to comment.