Skip to content

Commit 76c1a3e

Browse files
authored
Merge pull request #55 from euler-xyz/swapfee-test
test: ensure swap fees are accounted
2 parents 3692902 + dd7e843 commit 76c1a3e

File tree

3 files changed

+171
-38
lines changed

3 files changed

+171
-38
lines changed

src/EulerSwapHook.sol

Lines changed: 41 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -51,36 +51,53 @@ contract EulerSwapHook is EulerSwap, BaseHook {
5151
returns (bytes4, BeforeSwapDelta, uint24)
5252
{
5353
// determine inbound/outbound token based on 0->1 or 1->0 swap
54-
(Currency inputCurrency, Currency outputCurrency) =
55-
params.zeroForOne ? (key.currency0, key.currency1) : (key.currency1, key.currency0);
56-
bool isExactInput = params.amountSpecified < 0;
54+
bool zeroForOne = params.zeroForOne;
5755

58-
uint256 amountIn;
56+
uint256 amountInWithoutFee;
5957
uint256 amountOut;
58+
BeforeSwapDelta returnDelta;
6059

61-
if (isExactInput) {
62-
amountIn = uint256(-params.amountSpecified);
63-
amountOut = computeQuote(params.zeroForOne, uint256(-params.amountSpecified), true);
64-
} else {
65-
amountIn = computeQuote(params.zeroForOne, uint256(params.amountSpecified), false);
66-
amountOut = uint256(params.amountSpecified);
67-
}
68-
69-
// take the input token, from the PoolManager to the Euler vault
70-
// the debt will be paid by the swapper via the swap router
71-
// TODO: can we optimize the transfer by pulling from PoolManager directly to Euler?
72-
poolManager.take(inputCurrency, address(this), amountIn);
73-
depositAssets(inputCurrency == key.currency0 ? vault0 : vault1, amountIn);
60+
{
61+
(Currency inputCurrency, Currency outputCurrency) =
62+
zeroForOne ? (key.currency0, key.currency1) : (key.currency1, key.currency0);
63+
64+
uint256 amountIn;
65+
bool isExactInput = params.amountSpecified < 0;
66+
if (isExactInput) {
67+
amountIn = uint256(-params.amountSpecified);
68+
amountOut = computeQuote(zeroForOne, uint256(-params.amountSpecified), true);
69+
} else {
70+
amountIn = computeQuote(zeroForOne, uint256(params.amountSpecified), false);
71+
amountOut = uint256(params.amountSpecified);
72+
}
7473

75-
// pay the output token, to the PoolManager from an Euler vault
76-
// the credit will be forwarded to the swap router, which then forwards it to the swapper
77-
poolManager.sync(outputCurrency);
78-
withdrawAssets(outputCurrency == key.currency0 ? vault0 : vault1, amountOut, address(poolManager));
79-
poolManager.settle();
74+
// return the delta to the PoolManager, so it can process the accounting
75+
// exact input:
76+
// specifiedDelta = positive, to offset the input token taken by the hook (negative delta)
77+
// unspecifiedDelta = negative, to offset the credit of the output token paid by the hook (positive delta)
78+
// exact output:
79+
// specifiedDelta = negative, to offset the output token paid by the hook (positive delta)
80+
// unspecifiedDelta = positive, to offset the input token taken by the hook (negative delta)
81+
returnDelta = isExactInput
82+
? toBeforeSwapDelta(amountIn.toInt128(), -(amountOut.toInt128()))
83+
: toBeforeSwapDelta(-(amountOut.toInt128()), amountIn.toInt128());
84+
85+
// take the input token, from the PoolManager to the Euler vault
86+
// the debt will be paid by the swapper via the swap router
87+
// TODO: can we optimize the transfer by pulling from PoolManager directly to Euler?
88+
poolManager.take(inputCurrency, address(this), amountIn);
89+
amountInWithoutFee = depositAssets(zeroForOne ? vault0 : vault1, amountIn) * feeMultiplier / 1e18;
90+
91+
// pay the output token, to the PoolManager from an Euler vault
92+
// the credit will be forwarded to the swap router, which then forwards it to the swapper
93+
poolManager.sync(outputCurrency);
94+
withdrawAssets(zeroForOne ? vault1 : vault0, amountOut, address(poolManager));
95+
poolManager.settle();
96+
}
8097

8198
{
82-
uint256 newReserve0 = inputCurrency == key.currency0 ? (reserve0 + amountIn) : (reserve0 - amountOut);
83-
uint256 newReserve1 = inputCurrency == key.currency1 ? (reserve1 + amountIn) : (reserve1 - amountOut);
99+
uint256 newReserve0 = zeroForOne ? (reserve0 + amountInWithoutFee) : (reserve0 - amountOut);
100+
uint256 newReserve1 = !zeroForOne ? (reserve1 + amountInWithoutFee) : (reserve1 - amountOut);
84101

85102
require(newReserve0 <= type(uint112).max && newReserve1 <= type(uint112).max, Overflow());
86103
require(verify(newReserve0, newReserve1), CurveViolation());
@@ -89,16 +106,6 @@ contract EulerSwapHook is EulerSwap, BaseHook {
89106
reserve1 = uint112(newReserve1);
90107
}
91108

92-
// return the delta to the PoolManager, so it can process the accounting
93-
// exact input:
94-
// specifiedDelta = positive, to offset the input token taken by the hook (negative delta)
95-
// unspecifiedDelta = negative, to offset the credit of the output token paid by the hook (positive delta)
96-
// exact output:
97-
// specifiedDelta = negative, to offset the output token paid by the hook (positive delta)
98-
// unspecifiedDelta = positive, to offset the input token taken by the hook (negative delta)
99-
BeforeSwapDelta returnDelta = isExactInput
100-
? toBeforeSwapDelta(amountIn.toInt128(), -(amountOut.toInt128()))
101-
: toBeforeSwapDelta(-(amountOut.toInt128()), amountIn.toInt128());
102109
return (BaseHook.beforeSwap.selector, returnDelta, 0);
103110
}
104111

test/EulerSwapHook.fees.t.sol

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
pragma solidity ^0.8.24;
3+
4+
import {EulerSwapTestBase, EulerSwap, EulerSwapPeriphery, IEulerSwap} from "./EulerSwapTestBase.t.sol";
5+
import {TestERC20} from "evk-test/unit/evault/EVaultTestBase.t.sol";
6+
import {EulerSwapHook} from "../src/EulerSwapHook.sol";
7+
8+
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
9+
import {IPoolManager, PoolManagerDeployer} from "./utils/PoolManagerDeployer.sol";
10+
import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol";
11+
import {MinimalRouter} from "./utils/MinimalRouter.sol";
12+
import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol";
13+
import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
14+
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
15+
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
16+
17+
contract EulerSwapHookTest is EulerSwapTestBase {
18+
using StateLibrary for IPoolManager;
19+
20+
EulerSwapHook public eulerSwap;
21+
22+
IPoolManager public poolManager;
23+
PoolSwapTest public swapRouter;
24+
MinimalRouter public minimalRouter;
25+
26+
PoolSwapTest.TestSettings public settings = PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
27+
28+
function setUp() public virtual override {
29+
super.setUp();
30+
31+
poolManager = PoolManagerDeployer.deploy(address(this));
32+
swapRouter = new PoolSwapTest(poolManager);
33+
minimalRouter = new MinimalRouter(poolManager);
34+
35+
// set swap fee to 10 bips
36+
eulerSwap = createEulerSwapHook(poolManager, 60e18, 60e18, 0.001e18, 1e18, 1e18, 0.4e18, 0.85e18);
37+
eulerSwap.activate();
38+
39+
// confirm pool was created
40+
assertFalse(eulerSwap.poolKey().currency1 == CurrencyLibrary.ADDRESS_ZERO);
41+
(uint160 sqrtPriceX96,,,) = poolManager.getSlot0(eulerSwap.poolKey().toId());
42+
assertNotEq(sqrtPriceX96, 0);
43+
}
44+
45+
function test_SwapExactIn_withLpFee() public {
46+
int256 origNav = getHolderNAV();
47+
(uint112 r0, uint112 r1,) = eulerSwap.getReserves();
48+
49+
uint256 amountIn = 1e18;
50+
uint256 amountInWithoutFee = amountIn * eulerSwap.feeMultiplier() / 1e18;
51+
uint256 amountOut =
52+
periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), amountIn);
53+
54+
assetTST.mint(anyone, amountIn);
55+
56+
vm.startPrank(anyone);
57+
assetTST.approve(address(minimalRouter), amountIn);
58+
59+
bool zeroForOne = address(assetTST) < address(assetTST2);
60+
BalanceDelta result = minimalRouter.swap(eulerSwap.poolKey(), zeroForOne, amountIn, 0, "");
61+
vm.stopPrank();
62+
63+
assertEq(assetTST.balanceOf(anyone), 0);
64+
assertEq(assetTST2.balanceOf(anyone), amountOut);
65+
66+
assertEq(zeroForOne ? uint256(-int256(result.amount0())) : uint256(-int256(result.amount1())), amountIn);
67+
assertEq(zeroForOne ? uint256(int256(result.amount1())) : uint256(int256(result.amount0())), amountOut);
68+
69+
// assert fees were not added to the reserves
70+
(uint112 r0New, uint112 r1New,) = eulerSwap.getReserves();
71+
if (zeroForOne) {
72+
assertEq(r0New, r0 + amountInWithoutFee);
73+
assertEq(r1New, r1 - amountOut);
74+
} else {
75+
// oneForZero, so the curve received asset1
76+
assertEq(r0New, r0 - amountOut);
77+
assertEq(r1New, r1 + amountInWithoutFee);
78+
}
79+
80+
assertGt(getHolderNAV(), origNav + int256(amountIn - amountInWithoutFee));
81+
}
82+
83+
function test_SwapExactOut_withLpFee() public {
84+
int256 origNav = getHolderNAV();
85+
(uint112 r0, uint112 r1,) = eulerSwap.getReserves();
86+
87+
uint256 amountOut = 1e18;
88+
uint256 amountIn =
89+
periphery.quoteExactOutput(address(eulerSwap), address(assetTST), address(assetTST2), amountOut);
90+
91+
// inverse of the fee math in Periphery
92+
uint256 amountInWithoutFee = (amountIn * eulerSwap.feeMultiplier() - eulerSwap.feeMultiplier()) / 1e18;
93+
94+
assetTST.mint(anyone, amountIn);
95+
96+
vm.startPrank(anyone);
97+
assetTST.approve(address(minimalRouter), amountIn);
98+
99+
bool zeroForOne = address(assetTST) < address(assetTST2);
100+
BalanceDelta result = minimalRouter.swap(eulerSwap.poolKey(), zeroForOne, amountIn, amountOut, "");
101+
vm.stopPrank();
102+
103+
assertEq(assetTST.balanceOf(anyone), 0);
104+
assertEq(assetTST2.balanceOf(anyone), amountOut);
105+
106+
assertEq(zeroForOne ? uint256(-int256(result.amount0())) : uint256(-int256(result.amount1())), amountIn);
107+
assertEq(zeroForOne ? uint256(int256(result.amount1())) : uint256(int256(result.amount0())), amountOut);
108+
109+
// assert fees were not added to the reserves
110+
(uint112 r0New, uint112 r1New,) = eulerSwap.getReserves();
111+
if (zeroForOne) {
112+
assertEq(r0New, r0 + amountInWithoutFee + 1); // 1 wei of imprecision
113+
assertEq(r1New, r1 - amountOut);
114+
} else {
115+
// oneForZero, so the curve received asset1
116+
assertEq(r0New, r0 - amountOut);
117+
assertEq(r1New, r1 + amountInWithoutFee);
118+
}
119+
120+
assertGt(getHolderNAV(), origNav + int256(amountIn - amountInWithoutFee));
121+
}
122+
}

test/Fees.t.sol

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,17 @@ contract FeesTest is EulerSwapTestBase {
2020
// No fees
2121

2222
uint256 amountInNoFees = 1e18;
23-
uint256 amountOutNoFees = periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), amountInNoFees);
23+
uint256 amountOutNoFees =
24+
periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), amountInNoFees);
2425
assertApproxEqAbs(amountOutNoFees, 0.9983e18, 0.0001e18);
2526

2627
// With fees: Increase input amount so that corresponding output amount matches
2728

2829
eulerSwap = createEulerSwap(60e18, 60e18, fee, 1e18, 1e18, 0.9e18, 0.9e18);
2930

3031
uint256 amountIn = amountInNoFees * 1e18 / (1e18 - fee);
31-
uint256 amountOut = periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), amountIn);
32+
uint256 amountOut =
33+
periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), amountIn);
3234
assertApproxEqAbs(amountOut, amountOutNoFees, 1); // Same except for possible rounding down by 1
3335

3436
// Actually execute swap
@@ -68,14 +70,16 @@ contract FeesTest is EulerSwapTestBase {
6870
// No fees
6971

7072
uint256 amountOut = 1e18;
71-
uint256 amountInNoFees = periphery.quoteExactOutput(address(eulerSwap), address(assetTST), address(assetTST2), amountOut);
73+
uint256 amountInNoFees =
74+
periphery.quoteExactOutput(address(eulerSwap), address(assetTST), address(assetTST2), amountOut);
7275
assertApproxEqAbs(amountInNoFees, 1.0017e18, 0.0001e18);
7376

7477
// With fees: Increase input amount so output amount stays same
7578

7679
eulerSwap = createEulerSwap(60e18, 60e18, fee, 1e18, 1e18, 0.9e18, 0.9e18);
7780

78-
uint256 amountIn = periphery.quoteExactOutput(address(eulerSwap), address(assetTST), address(assetTST2), amountOut);
81+
uint256 amountIn =
82+
periphery.quoteExactOutput(address(eulerSwap), address(assetTST), address(assetTST2), amountOut);
7983
assertApproxEqAbs(amountIn, amountInNoFees * 1e18 / (1e18 - fee), 1); // Same except for possible rounding up by 1
8084

8185
// Actually execute swap

0 commit comments

Comments
 (0)