Skip to content

Commit dc22f5a

Browse files
test: exchange helpers V2
1 parent 411d1b7 commit dc22f5a

File tree

3 files changed

+118
-62
lines changed

3 files changed

+118
-62
lines changed

contracts/src/Zappers/Interfaces/IExchangeHelpersV2.sol

+2-4
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22

33
pragma solidity ^0.8.0;
44

5-
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
6-
75
interface IExchangeHelpersV2 {
8-
function getDy(uint256 _dx, bool _collToBold, IERC20 _collToken) external returns (uint256 dy);
9-
function getDx(uint256 _dy, bool _collToBold, IERC20 _collToken) external returns (uint256 dx);
6+
function getDy(uint256 _dx, bool _collToBold, address _collToken) external returns (uint256 dy);
7+
function getDx(uint256 _dy, bool _collToBold, address _collToken) external returns (uint256 dx);
108
}

contracts/src/Zappers/Modules/Exchanges/HybridCurveUniV3ExchangeHelpersV2.sol

+18-20
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,31 @@
22

33
pragma solidity ^0.8.18;
44

5-
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
6-
import {IWETH} from "../../../Interfaces/IWETH.sol";
75
import {ICurveStableswapNGPool} from "./Curve/ICurveStableswapNGPool.sol";
86
import {IQuoterV2} from "./UniswapV3/IQuoterV2.sol";
97
import {IExchangeHelpersV2} from "../../Interfaces/IExchangeHelpersV2.sol";
108

119
contract HybridCurveUniV3ExchangeHelpersV2 is IExchangeHelpersV2 {
12-
IERC20 public immutable USDC;
13-
IWETH public immutable WETH;
10+
address public immutable USDC;
11+
address public immutable WETH;
1412

1513
// Curve
1614
ICurveStableswapNGPool public immutable curvePool;
17-
uint128 public immutable USDC_INDEX;
18-
uint128 public immutable BOLD_TOKEN_INDEX;
15+
int128 public immutable USDC_INDEX;
16+
int128 public immutable BOLD_TOKEN_INDEX;
1917

2018
// Uniswap
2119
uint24 public immutable feeUsdcWeth;
2220
uint24 public immutable feeWethColl;
2321
IQuoterV2 public immutable uniV3Quoter;
2422

2523
constructor(
26-
IERC20 _usdc,
27-
IWETH _weth,
24+
address _usdc,
25+
address _weth,
2826
// Curve
2927
ICurveStableswapNGPool _curvePool,
30-
uint128 _usdcIndex,
31-
uint128 _boldIndex,
28+
int128 _usdcIndex,
29+
int128 _boldIndex,
3230
// UniV3
3331
uint24 _feeUsdcWeth,
3432
uint24 _feeWethColl,
@@ -48,11 +46,11 @@ contract HybridCurveUniV3ExchangeHelpersV2 is IExchangeHelpersV2 {
4846
uniV3Quoter = _uniV3Quoter;
4947
}
5048

51-
function getDy(uint256 _dx, bool _collToBold, IERC20 _collToken) external returns (uint256 dy) {
49+
function getDy(uint256 _dx, bool _collToBold, address _collToken) external returns (uint256 dy) {
5250
if (_collToBold) {
5351
// (Coll ->) WETH -> USDC?
5452
bytes memory path;
55-
if (address(WETH) == address(_collToken)) {
53+
if (WETH == _collToken) {
5654
path = abi.encodePacked(WETH, feeUsdcWeth, USDC);
5755
} else {
5856
path = abi.encodePacked(_collToken, feeWethColl, WETH, feeUsdcWeth, USDC);
@@ -61,14 +59,14 @@ contract HybridCurveUniV3ExchangeHelpersV2 is IExchangeHelpersV2 {
6159
(uint256 uniDy,,,) = uniV3Quoter.quoteExactInput(path, _dx);
6260

6361
// USDC -> BOLD?
64-
dy = curvePool.get_dy(int128(USDC_INDEX), int128(BOLD_TOKEN_INDEX), uniDy);
62+
dy = curvePool.get_dy(USDC_INDEX, BOLD_TOKEN_INDEX, uniDy);
6563
} else {
6664
// BOLD -> USDC?
67-
uint256 curveDy = curvePool.get_dy(int128(BOLD_TOKEN_INDEX), int128(USDC_INDEX), _dx);
65+
uint256 curveDy = curvePool.get_dy(BOLD_TOKEN_INDEX, USDC_INDEX, _dx);
6866

6967
// USDC -> WETH (-> Coll)?
7068
bytes memory path;
71-
if (address(WETH) == address(_collToken)) {
69+
if (WETH == _collToken) {
7270
path = abi.encodePacked(USDC, feeUsdcWeth, WETH);
7371
} else {
7472
path = abi.encodePacked(USDC, feeUsdcWeth, WETH, feeWethColl, _collToken);
@@ -78,15 +76,15 @@ contract HybridCurveUniV3ExchangeHelpersV2 is IExchangeHelpersV2 {
7876
}
7977
}
8078

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

8684
// Uniswap expects path to be reversed when quoting exact output
8785
// USDC <- WETH (<- Coll)?
8886
bytes memory path;
89-
if (address(WETH) == address(_collToken)) {
87+
if (WETH == _collToken) {
9088
path = abi.encodePacked(USDC, feeUsdcWeth, WETH);
9189
} else {
9290
path = abi.encodePacked(USDC, feeUsdcWeth, WETH, feeWethColl, _collToken);
@@ -97,7 +95,7 @@ contract HybridCurveUniV3ExchangeHelpersV2 is IExchangeHelpersV2 {
9795
// Uniswap expects path to be reversed when quoting exact output
9896
// (Coll <-) WETH <- USDC?
9997
bytes memory path;
100-
if (address(WETH) == address(_collToken)) {
98+
if (WETH == _collToken) {
10199
path = abi.encodePacked(WETH, feeUsdcWeth, USDC);
102100
} else {
103101
path = abi.encodePacked(_collToken, feeWethColl, WETH, feeUsdcWeth, USDC);
@@ -106,7 +104,7 @@ contract HybridCurveUniV3ExchangeHelpersV2 is IExchangeHelpersV2 {
106104
(uint256 uniDx,,,) = uniV3Quoter.quoteExactOutput(path, _dy);
107105

108106
// BOLD? -> USDC
109-
dx = curvePool.get_dx(int128(BOLD_TOKEN_INDEX), int128(USDC_INDEX), uniDx);
107+
dx = curvePool.get_dx(BOLD_TOKEN_INDEX, USDC_INDEX, uniDx);
110108
}
111109
}
112110
}

contracts/test/ExchangeHelpers.t.sol

+98-38
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ pragma solidity 0.8.24;
44
import {Test} from "forge-std/Test.sol";
55
import {stdMath} from "forge-std/StdMath.sol";
66
import {IERC20Metadata as IERC20} from "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol";
7+
import {ICurveStableswapNGPool} from "../src/Zappers/Modules/Exchanges/Curve/ICurveStableswapNGPool.sol";
78
import {IQuoterV2} from "../src/Zappers/Modules/Exchanges/UniswapV3/IQuoterV2.sol";
89
import {ISwapRouter} from "../src/Zappers/Modules/Exchanges/UniswapV3/ISwapRouter.sol";
9-
import {HybridCurveUniV3ExchangeHelpers} from "../src/Zappers/Modules/Exchanges/HybridCurveUniV3ExchangeHelpers.sol";
10+
import {HybridCurveUniV3ExchangeHelpersV2} from "../src/Zappers/Modules/Exchanges/HybridCurveUniV3ExchangeHelpersV2.sol";
11+
import {IExchange} from "../src/Zappers/Interfaces/IExchange.sol";
12+
import {IExchangeHelpersV2} from "../src/Zappers/Interfaces/IExchangeHelpersV2.sol";
1013
import {UseDeployment} from "./Utils/UseDeployment.sol";
1114

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

59+
mapping(address collToken => IExchange) exchange;
60+
IExchangeHelpersV2 exchangeHelpersV2;
61+
5662
error QuoteResult(uint256 amount);
5763

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

6975
_loadDeploymentFromManifest("addresses/1.json");
76+
77+
for (uint256 i = 0; i < branches.length; ++i) {
78+
address collToken = address(branches[i].collToken);
79+
exchange[collToken] = branches[i].zapper.exchange();
80+
}
81+
82+
exchangeHelpersV2 = new HybridCurveUniV3ExchangeHelpersV2({
83+
_usdc: USDC,
84+
_weth: WETH,
85+
_curvePool: ICurveStableswapNGPool(address(curveUsdcBold)),
86+
_usdcIndex: int8(curveUsdcBold.coins(0) == USDC ? 0 : 1),
87+
_boldIndex: int8(curveUsdcBold.coins(0) == BOLD ? 0 : 1),
88+
_feeUsdcWeth: UNIV3_FEE_USDC_WETH,
89+
_feeWethColl: UNIV3_FEE_WETH_COLL,
90+
_uniV3Quoter: uniV3Quoter
91+
});
7092
}
7193

7294
function test_Curve_CanQuoteApproxDx(bool zeroToOne, uint256 dyExpected) external {
7395
(int128 i, int128 j) = zeroToOne ? (int128(0), int128(1)) : (int128(1), int128(0));
7496
(address inputToken, address outputToken) = (curveUsdcBold.coins(uint128(i)), curveUsdcBold.coins(uint128(j)));
7597
uint256 dyDecimals = IERC20(outputToken).decimals();
7698
uint256 dyDiv = 10 ** (18 - dyDecimals);
77-
dyExpected = bound(dyExpected, 1, 1_000_000 ether / dyDiv);
99+
dyExpected = bound(dyExpected, 1 ether / dyDiv, 1_000_000 ether / dyDiv);
78100

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

82104
uint256 balance0 = IERC20(outputToken).balanceOf(address(this));
83105
deal(inputToken, address(this), dx);
84106
IERC20(inputToken).approve(address(curveUsdcBold), dx);
85107
uint256 dy = curveUsdcBold.exchange(i, j, dx, 0);
86108

87109
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");
110+
assertApproxEqRelDecimal(dy, dyExpected, 1e-5 ether, dyDecimals, "dy !~= expected dy");
89111
}
90112

91113
function test_UniV3_CanQuoteApproxDx(bool collToUsdc, uint256 collIndex, uint256 dyExpected) external {
@@ -94,7 +116,9 @@ contract ExchangeHelpersTest is Test, UseDeployment {
94116
(address inputToken, address outputToken) = collToUsdc ? (collToken, USDC) : (USDC, collToken);
95117
uint256 dyDecimals = IERC20(outputToken).decimals();
96118
uint256 dyDiv = 10 ** (18 - dyDecimals);
97-
dyExpected = bound(dyExpected, 1, (collToUsdc ? 100_000 ether : 100 ether) / dyDiv);
119+
dyExpected = bound(
120+
dyExpected, (collToUsdc ? 1 ether : 0.001 ether) / dyDiv, (collToUsdc ? 100_000 ether : 100 ether) / dyDiv
121+
);
98122

99123
bytes[] memory pathUsdcToColl = new bytes[](collToken == WETH ? 3 : 5);
100124
pathUsdcToColl[0] = abi.encodePacked(USDC);
@@ -110,11 +134,10 @@ contract ExchangeHelpersTest is Test, UseDeployment {
110134
collToUsdc ? (pathCollToUsdc.join(), pathUsdcToColl.join()) : (pathUsdcToColl.join(), pathCollToUsdc.join());
111135

112136
uint256 dx = uniV3Quoter_quoteExactOutput(quotePath, dyExpected);
113-
// vm.assume(dx > 0); // Fine by Uniswap
114-
115137
uint256 balance0 = IERC20(outputToken).balanceOf(address(this));
116138
deal(inputToken, address(this), dx);
117139
IERC20(inputToken).approve(address(uniV3Router), dx);
140+
118141
uint256 dy = uniV3Router.exactInput(
119142
ISwapRouter.ExactInputParams({
120143
path: swapPath,
@@ -129,49 +152,86 @@ contract ExchangeHelpersTest is Test, UseDeployment {
129152
assertApproxEqAbsDecimal(dy, dyExpected, 4e-10 ether / dyDiv, dyDecimals, "dy !~= expected dy");
130153
}
131154

132-
function uniV3Quoter_throw_quoteExactOutput(bytes memory path, uint256 amountOut) external {
133-
(uint256 amountIn,,,) = uniV3Quoter.quoteExactOutput(path, amountOut);
134-
revert QuoteResult(amountIn);
155+
function test_ExchangeHelpersV2_CanQuoteApproxDx(bool collToBold, uint256 collIndex, uint256 dyExpected) external {
156+
collIndex = bound(collIndex, 0, branches.length - 1);
157+
address collToken = address(branches[collIndex].collToken);
158+
(address inputToken, address outputToken) = collToBold ? (collToken, BOLD) : (BOLD, collToken);
159+
dyExpected = bound(dyExpected, 1 ether, (collToBold ? 100_000 ether : 100 ether));
160+
161+
uint256 dx = exchangeHelpersV2_getDx(dyExpected, collToBold, collToken);
162+
uint256 balance0 = IERC20(outputToken).balanceOf(address(this));
163+
deal(inputToken, address(this), dx);
164+
IERC20(inputToken).approve(address(exchange[collToken]), dx);
165+
166+
if (collToBold) {
167+
exchange[collToken].swapToBold(dx, 0);
168+
} else {
169+
exchange[collToken].swapFromBold(dx, 0);
170+
}
171+
172+
uint256 dy = IERC20(outputToken).balanceOf(address(this)) - balance0;
173+
assertApproxEqRelDecimal(dy, dyExpected, 1e-5 ether, 18, "dy !~= expected dy");
135174
}
136175

137-
function _revert(bytes memory revertData) internal pure {
176+
function _revert(bytes memory revertData) private pure {
138177
assembly {
139178
revert(add(32, revertData), mload(revertData))
140179
}
141180
}
142181

143-
function uniV3Quoter_quoteExactOutput(bytes memory path, uint256 amountOut) internal returns (uint256 amountIn) {
144-
try this.uniV3Quoter_throw_quoteExactOutput(path, amountOut) {
182+
function _decodeQuoteResult(bytes memory revertData) private pure returns (uint256) {
183+
bytes4 selector = bytes4(revertData);
184+
if (selector == QuoteResult.selector && revertData.length == 4 + 32) {
185+
return uint256(bytes32(revertData.slice(4)));
186+
} else {
187+
_revert(revertData); // bubble
188+
}
189+
}
190+
191+
function uniV3Quoter_quoteExactOutput_throw(bytes memory path, uint256 amountOut) external {
192+
(uint256 amountIn,,,) = uniV3Quoter.quoteExactOutput(path, amountOut);
193+
revert QuoteResult(amountIn);
194+
}
195+
196+
function uniV3Quoter_quoteExactOutput(bytes memory path, uint256 amountOut) internal returns (uint256) {
197+
try this.uniV3Quoter_quoteExactOutput_throw(path, amountOut) {
145198
revert("Should have reverted");
146199
} 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-
}
200+
return _decodeQuoteResult(revertData);
153201
}
154202
}
155203

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");
204+
function exchangeHelpersV2_getDx_throw(uint256 dy, bool collToBold, address collToken) external {
205+
revert QuoteResult(exchangeHelpersV2.getDx(dy, collToBold, collToken));
206+
}
207+
208+
function exchangeHelpersV2_getDx(uint256 dy, bool collToBold, address collToken) internal returns (uint256) {
209+
try this.exchangeHelpersV2_getDx_throw(dy, collToBold, collToken) {
210+
revert("Should have reverted");
211+
} catch (bytes memory revertData) {
212+
return _decodeQuoteResult(revertData);
175213
}
176214
}
215+
216+
// function assertApproxEqAbsRelDecimal(
217+
// uint256 a,
218+
// uint256 b,
219+
// uint256 maxAbs,
220+
// uint256 maxRel,
221+
// uint256 decimals,
222+
// string memory err
223+
// ) internal pure {
224+
// uint256 abs = stdMath.delta(a, b);
225+
// uint256 rel = stdMath.percentDelta(a, b);
226+
227+
// if (abs > maxAbs && rel > maxRel) {
228+
// if (rel > maxRel) {
229+
// assertApproxEqRelDecimal(a, b, maxRel, decimals, err);
230+
// } else {
231+
// assertApproxEqAbsDecimal(a, b, maxAbs, decimals, err);
232+
// }
233+
234+
// revert("Assertion should have failed");
235+
// }
236+
// }
177237
}

0 commit comments

Comments
 (0)