Skip to content

Commit 628c4a2

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

File tree

3 files changed

+162
-60
lines changed

3 files changed

+162
-60
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

+142-36
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,12 @@ 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+
120+
if (collToUsdc) {
121+
dyExpected = bound(dyExpected, 1 ether / dyDiv, 100_000 ether / dyDiv);
122+
} else {
123+
dyExpected = bound(dyExpected, 0.001 ether / dyDiv, 100 ether / dyDiv);
124+
}
98125

99126
bytes[] memory pathUsdcToColl = new bytes[](collToken == WETH ? 3 : 5);
100127
pathUsdcToColl[0] = abi.encodePacked(USDC);
@@ -110,11 +137,10 @@ contract ExchangeHelpersTest is Test, UseDeployment {
110137
collToUsdc ? (pathCollToUsdc.join(), pathUsdcToColl.join()) : (pathUsdcToColl.join(), pathCollToUsdc.join());
111138

112139
uint256 dx = uniV3Quoter_quoteExactOutput(quotePath, dyExpected);
113-
// vm.assume(dx > 0); // Fine by Uniswap
114-
115140
uint256 balance0 = IERC20(outputToken).balanceOf(address(this));
116141
deal(inputToken, address(this), dx);
117142
IERC20(inputToken).approve(address(uniV3Router), dx);
143+
118144
uint256 dy = uniV3Router.exactInput(
119145
ISwapRouter.ExactInputParams({
120146
path: swapPath,
@@ -129,49 +155,129 @@ contract ExchangeHelpersTest is Test, UseDeployment {
129155
assertApproxEqAbsDecimal(dy, dyExpected, 4e-10 ether / dyDiv, dyDecimals, "dy !~= expected dy");
130156
}
131157

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

137-
function _revert(bytes memory revertData) internal pure {
184+
function test_ExchangeHelpersV2_CanQuoteExactDy(bool collToBold, uint256 collIndex, uint256 dx) external {
185+
collIndex = bound(collIndex, 0, branches.length - 1);
186+
address collToken = address(branches[collIndex].collToken);
187+
(address inputToken, address outputToken) = collToBold ? (collToken, BOLD) : (BOLD, collToken);
188+
189+
if (collToBold) {
190+
dx = bound(dx, 0.001 ether, 100 ether);
191+
} else {
192+
dx = bound(dx, 1 ether, 100_000 ether);
193+
}
194+
195+
uint256 dyExpected = exchangeHelpersV2_getDy(dx, collToBold, collToken);
196+
uint256 balance0 = IERC20(outputToken).balanceOf(address(this));
197+
deal(inputToken, address(this), dx);
198+
IERC20(inputToken).approve(address(exchange[collToken]), dx);
199+
200+
if (collToBold) {
201+
exchange[collToken].swapToBold(dx, 0);
202+
} else {
203+
exchange[collToken].swapFromBold(dx, 0);
204+
}
205+
206+
uint256 dy = IERC20(outputToken).balanceOf(address(this)) - balance0;
207+
assertEqDecimal(dy, dyExpected, 18, "dy != expected dy");
208+
}
209+
210+
function _revert(bytes memory revertData) private pure {
138211
assembly {
139212
revert(add(32, revertData), mload(revertData))
140213
}
141214
}
142215

143-
function uniV3Quoter_quoteExactOutput(bytes memory path, uint256 amountOut) internal returns (uint256 amountIn) {
144-
try this.uniV3Quoter_throw_quoteExactOutput(path, amountOut) {
216+
function _decodeQuoteResult(bytes memory revertData) private pure returns (uint256) {
217+
bytes4 selector = bytes4(revertData);
218+
if (selector == QuoteResult.selector && revertData.length == 4 + 32) {
219+
return uint256(bytes32(revertData.slice(4)));
220+
} else {
221+
_revert(revertData); // bubble
222+
}
223+
}
224+
225+
function uniV3Quoter_quoteExactOutput_throw(bytes memory path, uint256 amountOut) external {
226+
(uint256 amountIn,,,) = uniV3Quoter.quoteExactOutput(path, amountOut);
227+
revert QuoteResult(amountIn);
228+
}
229+
230+
function uniV3Quoter_quoteExactOutput(bytes memory path, uint256 amountOut) internal returns (uint256) {
231+
try this.uniV3Quoter_quoteExactOutput_throw(path, amountOut) {
145232
revert("Should have reverted");
146233
} 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-
}
234+
return _decodeQuoteResult(revertData);
153235
}
154236
}
155237

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);
238+
function exchangeHelpersV2_getDx_throw(uint256 dy, bool collToBold, address collToken) external {
239+
revert QuoteResult(exchangeHelpersV2.getDx(dy, collToBold, collToken));
240+
}
241+
242+
function exchangeHelpersV2_getDx(uint256 dy, bool collToBold, address collToken) internal returns (uint256) {
243+
try this.exchangeHelpersV2_getDx_throw(dy, collToBold, collToken) {
244+
revert("Should have reverted");
245+
} catch (bytes memory revertData) {
246+
return _decodeQuoteResult(revertData);
247+
}
248+
}
166249

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-
}
250+
function exchangeHelpersV2_getDy_throw(uint256 dx, bool collToBold, address collToken) external {
251+
revert QuoteResult(exchangeHelpersV2.getDy(dx, collToBold, collToken));
252+
}
173253

174-
revert("Assertion should have failed");
254+
function exchangeHelpersV2_getDy(uint256 dx, bool collToBold, address collToken) internal returns (uint256) {
255+
try this.exchangeHelpersV2_getDy_throw(dx, collToBold, collToken) {
256+
revert("Should have reverted");
257+
} catch (bytes memory revertData) {
258+
return _decodeQuoteResult(revertData);
175259
}
176260
}
261+
262+
// function assertApproxEqAbsRelDecimal(
263+
// uint256 a,
264+
// uint256 b,
265+
// uint256 maxAbs,
266+
// uint256 maxRel,
267+
// uint256 decimals,
268+
// string memory err
269+
// ) internal pure {
270+
// uint256 abs = stdMath.delta(a, b);
271+
// uint256 rel = stdMath.percentDelta(a, b);
272+
273+
// if (abs > maxAbs && rel > maxRel) {
274+
// if (rel > maxRel) {
275+
// assertApproxEqRelDecimal(a, b, maxRel, decimals, err);
276+
// } else {
277+
// assertApproxEqAbsDecimal(a, b, maxAbs, decimals, err);
278+
// }
279+
280+
// revert("Assertion should have failed");
281+
// }
282+
// }
177283
}

0 commit comments

Comments
 (0)