Skip to content

Commit 411d1b7

Browse files
test: show how approx dx can be quoted
1 parent 4c8b54a commit 411d1b7

File tree

3 files changed

+186
-1
lines changed

3 files changed

+186
-1
lines changed

contracts/foundry.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ ignored_error_codes = [3860, 5574] # contract-size
99
fs_permissions = [
1010
{ access = "read", path = "./utils/assets/" },
1111
{ access = "read-write", path = "./utils/assets/test_output" },
12-
{ access = "read-write", path = "./deployment-manifest.json" }
12+
{ access = "read-write", path = "./deployment-manifest.json" },
13+
{ access = "read", path = "./addresses/" }
1314
]
1415

1516
[invariant]

contracts/test/ExchangeHelpers.t.sol

+177
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.24;
3+
4+
import {Test} from "forge-std/Test.sol";
5+
import {stdMath} from "forge-std/StdMath.sol";
6+
import {IERC20Metadata as IERC20} from "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol";
7+
import {IQuoterV2} from "../src/Zappers/Modules/Exchanges/UniswapV3/IQuoterV2.sol";
8+
import {ISwapRouter} from "../src/Zappers/Modules/Exchanges/UniswapV3/ISwapRouter.sol";
9+
import {HybridCurveUniV3ExchangeHelpers} from "../src/Zappers/Modules/Exchanges/HybridCurveUniV3ExchangeHelpers.sol";
10+
import {UseDeployment} from "./Utils/UseDeployment.sol";
11+
12+
library Bytes {
13+
function slice(bytes memory array, uint256 start) internal pure returns (bytes memory sliced) {
14+
sliced = new bytes(array.length - start);
15+
16+
for (uint256 i = 0; i < sliced.length; ++i) {
17+
sliced[i] = array[start + i];
18+
}
19+
}
20+
}
21+
22+
library BytesArray {
23+
function clone(bytes[] memory array) internal pure returns (bytes[] memory cloned) {
24+
cloned = new bytes[](array.length);
25+
26+
for (uint256 i = 0; i < array.length; ++i) {
27+
cloned[i] = array[i];
28+
}
29+
}
30+
31+
function reverse(bytes[] memory array) internal pure returns (bytes[] memory) {
32+
for ((uint256 i, uint256 j) = (0, array.length - 1); i < j; (++i, --j)) {
33+
(array[i], array[j]) = (array[j], array[i]);
34+
}
35+
36+
return array;
37+
}
38+
39+
function join(bytes[] memory array) internal pure returns (bytes memory joined) {
40+
for (uint256 i = 0; i < array.length; ++i) {
41+
joined = bytes.concat(joined, array[i]);
42+
}
43+
}
44+
}
45+
46+
contract ExchangeHelpersTest is Test, UseDeployment {
47+
using Bytes for bytes;
48+
using BytesArray for bytes[];
49+
50+
uint24 constant UNIV3_FEE_USDC_WETH = 500; // 0.05%
51+
uint24 constant UNIV3_FEE_WETH_COLL = 100; // 0.01%
52+
53+
IQuoterV2 constant uniV3Quoter = IQuoterV2(0x61fFE014bA17989E743c5F6cB21bF9697530B21e);
54+
ISwapRouter constant uniV3Router = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564);
55+
56+
error QuoteResult(uint256 amount);
57+
58+
function setUp() external {
59+
string memory rpcUrl = vm.envOr("MAINNET_RPC_URL", string(""));
60+
if (bytes(rpcUrl).length == 0) vm.skip(true);
61+
62+
uint256 forkBlock = vm.envOr("FORK_BLOCK", uint256(0));
63+
if (forkBlock != 0) {
64+
vm.createSelectFork(rpcUrl, forkBlock);
65+
} else {
66+
vm.createSelectFork(rpcUrl);
67+
}
68+
69+
_loadDeploymentFromManifest("addresses/1.json");
70+
}
71+
72+
function test_Curve_CanQuoteApproxDx(bool zeroToOne, uint256 dyExpected) external {
73+
(int128 i, int128 j) = zeroToOne ? (int128(0), int128(1)) : (int128(1), int128(0));
74+
(address inputToken, address outputToken) = (curveUsdcBold.coins(uint128(i)), curveUsdcBold.coins(uint128(j)));
75+
uint256 dyDecimals = IERC20(outputToken).decimals();
76+
uint256 dyDiv = 10 ** (18 - dyDecimals);
77+
dyExpected = bound(dyExpected, 1, 1_000_000 ether / dyDiv);
78+
79+
uint256 dx = curveUsdcBold.get_dx(i, j, dyExpected);
80+
vm.assume(dx > 0); // Curve reverts in this case
81+
82+
uint256 balance0 = IERC20(outputToken).balanceOf(address(this));
83+
deal(inputToken, address(this), dx);
84+
IERC20(inputToken).approve(address(curveUsdcBold), dx);
85+
uint256 dy = curveUsdcBold.exchange(i, j, dx, 0);
86+
87+
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");
89+
}
90+
91+
function test_UniV3_CanQuoteApproxDx(bool collToUsdc, uint256 collIndex, uint256 dyExpected) external {
92+
collIndex = bound(collIndex, 0, branches.length - 1);
93+
address collToken = address(branches[collIndex].collToken);
94+
(address inputToken, address outputToken) = collToUsdc ? (collToken, USDC) : (USDC, collToken);
95+
uint256 dyDecimals = IERC20(outputToken).decimals();
96+
uint256 dyDiv = 10 ** (18 - dyDecimals);
97+
dyExpected = bound(dyExpected, 1, (collToUsdc ? 100_000 ether : 100 ether) / dyDiv);
98+
99+
bytes[] memory pathUsdcToColl = new bytes[](collToken == WETH ? 3 : 5);
100+
pathUsdcToColl[0] = abi.encodePacked(USDC);
101+
pathUsdcToColl[1] = abi.encodePacked(UNIV3_FEE_USDC_WETH);
102+
pathUsdcToColl[2] = abi.encodePacked(WETH);
103+
if (collToken != WETH) {
104+
pathUsdcToColl[3] = abi.encodePacked(UNIV3_FEE_WETH_COLL);
105+
pathUsdcToColl[4] = abi.encodePacked(collToken);
106+
}
107+
108+
bytes[] memory pathCollToUsdc = pathUsdcToColl.clone().reverse();
109+
(bytes memory swapPath, bytes memory quotePath) =
110+
collToUsdc ? (pathCollToUsdc.join(), pathUsdcToColl.join()) : (pathUsdcToColl.join(), pathCollToUsdc.join());
111+
112+
uint256 dx = uniV3Quoter_quoteExactOutput(quotePath, dyExpected);
113+
// vm.assume(dx > 0); // Fine by Uniswap
114+
115+
uint256 balance0 = IERC20(outputToken).balanceOf(address(this));
116+
deal(inputToken, address(this), dx);
117+
IERC20(inputToken).approve(address(uniV3Router), dx);
118+
uint256 dy = uniV3Router.exactInput(
119+
ISwapRouter.ExactInputParams({
120+
path: swapPath,
121+
recipient: address(this),
122+
deadline: block.timestamp,
123+
amountIn: dx,
124+
amountOutMinimum: 0
125+
})
126+
);
127+
128+
assertEqDecimal(IERC20(outputToken).balanceOf(address(this)) - balance0, dy, dyDecimals, "balance != dy");
129+
assertApproxEqAbsDecimal(dy, dyExpected, 4e-10 ether / dyDiv, dyDecimals, "dy !~= expected dy");
130+
}
131+
132+
function uniV3Quoter_throw_quoteExactOutput(bytes memory path, uint256 amountOut) external {
133+
(uint256 amountIn,,,) = uniV3Quoter.quoteExactOutput(path, amountOut);
134+
revert QuoteResult(amountIn);
135+
}
136+
137+
function _revert(bytes memory revertData) internal pure {
138+
assembly {
139+
revert(add(32, revertData), mload(revertData))
140+
}
141+
}
142+
143+
function uniV3Quoter_quoteExactOutput(bytes memory path, uint256 amountOut) internal returns (uint256 amountIn) {
144+
try this.uniV3Quoter_throw_quoteExactOutput(path, amountOut) {
145+
revert("Should have reverted");
146+
} 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+
}
153+
}
154+
}
155+
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");
175+
}
176+
}
177+
}

contracts/test/Utils/UseDeployment.sol

+7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol";
88
import {IUserProxy} from "V2-gov/src/interfaces/IUserProxy.sol";
99
import {CurveV2GaugeRewards} from "V2-gov/src/CurveV2GaugeRewards.sol";
1010
import {Governance} from "V2-gov/src/Governance.sol";
11+
import {IExchangeHelpers} from "src/Zappers/Interfaces/IExchangeHelpers.sol";
1112
import {ILeverageZapper} from "src/Zappers/Interfaces/ILeverageZapper.sol";
1213
import {IZapper} from "src/Zappers/Interfaces/IZapper.sol";
1314
import {IActivePool} from "src/Interfaces/IActivePool.sol";
@@ -73,6 +74,7 @@ contract UseDeployment is CommonBase {
7374
ICollateralRegistry collateralRegistry;
7475
IBoldToken boldToken;
7576
IHintHelpers hintHelpers;
77+
IExchangeHelpers exchangeHelpers;
7678
Governance governance;
7779
ICurveStableSwapNG curveUsdcBold;
7880
ILiquidityGaugeV6 curveUsdcBoldGauge;
@@ -88,6 +90,7 @@ contract UseDeployment is CommonBase {
8890
collateralRegistry = ICollateralRegistry(json.readAddress(".collateralRegistry"));
8991
boldToken = IBoldToken(BOLD = json.readAddress(".boldToken"));
9092
hintHelpers = IHintHelpers(json.readAddress(".hintHelpers"));
93+
exchangeHelpers = IExchangeHelpers(json.readAddress(".exchangeHelpers"));
9194
governance = Governance(json.readAddress(".governance.governance"));
9295
curveUsdcBold = ICurveStableSwapNG(json.readAddress(".governance.curveUsdcBoldPool"));
9396
curveUsdcBoldGauge = ILiquidityGaugeV6(json.readAddress(".governance.curveUsdcBoldGauge"));
@@ -99,10 +102,14 @@ contract UseDeployment is CommonBase {
99102

100103
vm.label(address(collateralRegistry), "CollateralRegistry");
101104
vm.label(address(hintHelpers), "HintHelpers");
105+
vm.label(address(exchangeHelpers), "ExchangeHelpers");
102106
vm.label(address(governance), "Governance");
103107
vm.label(address(curveUsdcBold), "CurveStableSwapNG");
104108
vm.label(address(curveUsdcBoldGauge), "LiquidityGaugeV6");
105109
vm.label(address(curveUsdcBoldInitiative), "CurveV2GaugeRewards");
110+
vm.label(address(curveLusdBold), "CurveStableSwapNG");
111+
vm.label(address(curveLusdBoldGauge), "LiquidityGaugeV6");
112+
vm.label(address(curveLusdBoldInitiative), "CurveV2GaugeRewards");
106113

107114
ETH_GAS_COMPENSATION = json.readUint(".constants.ETH_GAS_COMPENSATION");
108115
MIN_DEBT = json.readUint(".constants.MIN_DEBT");

0 commit comments

Comments
 (0)