Skip to content

Commit 45501b7

Browse files
authored
Merge pull request #52 from euler-xyz/minimal-router
Minimal Router with Prepaid Inputs
2 parents 506d101 + 08252c3 commit 45501b7

File tree

2 files changed

+135
-8
lines changed

2 files changed

+135
-8
lines changed

test/EulerSwapHook.swaps.t.sol

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import {EulerSwapHook} from "../src/EulerSwapHook.sol";
88
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
99
import {IPoolManager, PoolManagerDeployer} from "./utils/PoolManagerDeployer.sol";
1010
import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol";
11+
import {MinimalRouter} from "./utils/MinimalRouter.sol";
1112
import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol";
1213
import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
1314
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
15+
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
1416

1517
contract EulerSwapHookTest is EulerSwapTestBase {
1618
using StateLibrary for IPoolManager;
@@ -19,6 +21,7 @@ contract EulerSwapHookTest is EulerSwapTestBase {
1921

2022
IPoolManager public poolManager;
2123
PoolSwapTest public swapRouter;
24+
MinimalRouter public minimalRouter;
2225

2326
PoolSwapTest.TestSettings public settings = PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
2427

@@ -27,6 +30,7 @@ contract EulerSwapHookTest is EulerSwapTestBase {
2730

2831
poolManager = PoolManagerDeployer.deploy(address(this));
2932
swapRouter = new PoolSwapTest(poolManager);
33+
minimalRouter = new MinimalRouter(poolManager);
3034

3135
eulerSwap = createEulerSwapHook(poolManager, 60e18, 60e18, 0, 1e18, 1e18, 0.4e18, 0.85e18);
3236
eulerSwap.activate();
@@ -35,10 +39,6 @@ contract EulerSwapHookTest is EulerSwapTestBase {
3539
assertFalse(eulerSwap.poolKey().currency1 == CurrencyLibrary.ADDRESS_ZERO);
3640
(uint160 sqrtPriceX96,,,) = poolManager.getSlot0(eulerSwap.poolKey().toId());
3741
assertNotEq(sqrtPriceX96, 0);
38-
39-
// Seed the poolManager with balance so that transient withdrawing before depositing succeeds
40-
assetTST.mint(address(poolManager), 1000e18);
41-
assetTST2.mint(address(poolManager), 1000e18);
4242
}
4343

4444
function test_SwapExactIn() public {
@@ -49,14 +49,34 @@ contract EulerSwapHookTest is EulerSwapTestBase {
4949
assetTST.mint(anyone, amountIn);
5050

5151
vm.startPrank(anyone);
52-
assetTST.approve(address(swapRouter), amountIn);
52+
assetTST.approve(address(minimalRouter), amountIn);
5353

5454
bool zeroForOne = address(assetTST) < address(assetTST2);
55-
_swap(eulerSwap.poolKey(), zeroForOne, true, amountIn);
55+
BalanceDelta result = minimalRouter.swap(eulerSwap.poolKey(), zeroForOne, amountIn, 0, "");
5656
vm.stopPrank();
5757

5858
assertEq(assetTST.balanceOf(anyone), 0);
5959
assertEq(assetTST2.balanceOf(anyone), amountOut);
60+
61+
assertEq(zeroForOne ? uint256(-int256(result.amount0())) : uint256(-int256(result.amount1())), amountIn);
62+
assertEq(zeroForOne ? uint256(int256(result.amount1())) : uint256(int256(result.amount0())), amountOut);
63+
}
64+
65+
/// @dev swapping with an amount that exceeds PoolManager's ERC20 token balance will revert
66+
/// if the router does not pre-pay the input
67+
function test_swapExactIn_revertWithoutTokenLiquidity() public {
68+
uint256 amountIn = 1e18; // input amount exceeds PoolManager balance
69+
70+
assetTST.mint(anyone, amountIn);
71+
72+
vm.startPrank(anyone);
73+
assetTST.approve(address(swapRouter), amountIn);
74+
75+
bool zeroForOne = address(assetTST) < address(assetTST2);
76+
PoolKey memory poolKey = eulerSwap.poolKey();
77+
vm.expectRevert();
78+
_swap(poolKey, zeroForOne, true, amountIn);
79+
vm.stopPrank();
6080
}
6181

6282
function test_SwapExactOut() public {
@@ -67,13 +87,35 @@ contract EulerSwapHookTest is EulerSwapTestBase {
6787
assetTST.mint(anyone, amountIn);
6888

6989
vm.startPrank(anyone);
70-
assetTST.approve(address(swapRouter), amountIn);
90+
assetTST.approve(address(minimalRouter), amountIn);
91+
7192
bool zeroForOne = address(assetTST) < address(assetTST2);
72-
_swap(eulerSwap.poolKey(), zeroForOne, false, amountOut);
93+
BalanceDelta result = minimalRouter.swap(eulerSwap.poolKey(), zeroForOne, amountIn, amountOut, "");
7394
vm.stopPrank();
7495

7596
assertEq(assetTST.balanceOf(anyone), 0);
7697
assertEq(assetTST2.balanceOf(anyone), amountOut);
98+
99+
assertEq(zeroForOne ? uint256(-int256(result.amount0())) : uint256(-int256(result.amount1())), amountIn);
100+
assertEq(zeroForOne ? uint256(int256(result.amount1())) : uint256(int256(result.amount0())), amountOut);
101+
}
102+
103+
/// @dev swapping with an amount that exceeds PoolManager's ERC20 token balance will revert
104+
/// if the router does not pre-pay the input
105+
function test_SwapExactOut_revertWithoutTokenLiquidity() public {
106+
uint256 amountOut = 1e18;
107+
uint256 amountIn =
108+
periphery.quoteExactOutput(address(eulerSwap), address(assetTST), address(assetTST2), amountOut);
109+
110+
assetTST.mint(anyone, amountIn);
111+
112+
vm.startPrank(anyone);
113+
assetTST.approve(address(swapRouter), amountIn);
114+
bool zeroForOne = address(assetTST) < address(assetTST2);
115+
PoolKey memory poolKey = eulerSwap.poolKey();
116+
vm.expectRevert();
117+
_swap(poolKey, zeroForOne, false, amountOut);
118+
vm.stopPrank();
77119
}
78120

79121
function _swap(PoolKey memory key, bool zeroForOne, bool exactInput, uint256 amount) internal {

test/utils/MinimalRouter.sol

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.24;
3+
4+
import {CurrencyLibrary, Currency} from "@uniswap/v4-core/src/types/Currency.sol";
5+
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
6+
import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
7+
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
8+
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
9+
import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
10+
import {CurrencySettler} from "@uniswap/v4-core/test/utils/CurrencySettler.sol";
11+
import {SafeCallback} from "v4-periphery/src/base/SafeCallback.sol";
12+
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
13+
import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol";
14+
15+
contract MinimalRouter is SafeCallback {
16+
using TransientStateLibrary for IPoolManager;
17+
using CurrencySettler for Currency;
18+
19+
uint160 public constant MIN_PRICE_LIMIT = TickMath.MIN_SQRT_PRICE + 1;
20+
uint160 public constant MAX_PRICE_LIMIT = TickMath.MAX_SQRT_PRICE - 1;
21+
22+
constructor(IPoolManager _manager) SafeCallback(_manager) {}
23+
24+
/// @dev an unsafe swap function that does not check for slippage
25+
/// @param key The pool key
26+
/// @param zeroForOne The direction of the swap
27+
/// @param amountIn The amount of input token, should be provided (as an estimate) for exact output swaps
28+
/// @param amountOut The amount of output token can be provided as 0, for exact input swaps
29+
/// @param hookData The data to pass to the hook
30+
function swap(PoolKey memory key, bool zeroForOne, uint256 amountIn, uint256 amountOut, bytes memory hookData)
31+
external
32+
payable
33+
returns (BalanceDelta delta)
34+
{
35+
delta = abi.decode(
36+
poolManager.unlock(abi.encode(msg.sender, key, zeroForOne, amountIn, amountOut, hookData)), (BalanceDelta)
37+
);
38+
39+
uint256 ethBalance = address(this).balance;
40+
if (ethBalance > 0) CurrencyLibrary.ADDRESS_ZERO.transfer(msg.sender, ethBalance);
41+
}
42+
43+
function _unlockCallback(bytes calldata data) internal override returns (bytes memory) {
44+
(
45+
address sender,
46+
PoolKey memory key,
47+
bool zeroForOne,
48+
uint256 amountIn,
49+
uint256 amountOut,
50+
bytes memory hookData
51+
) = abi.decode(data, (address, PoolKey, bool, uint256, uint256, bytes));
52+
53+
// send the input first to avoid PoolManager token balance issues
54+
zeroForOne
55+
? key.currency0.settle(poolManager, sender, amountIn, false)
56+
: key.currency1.settle(poolManager, sender, amountIn, false);
57+
58+
// execute the swap
59+
poolManager.swap(
60+
key,
61+
IPoolManager.SwapParams({
62+
zeroForOne: zeroForOne,
63+
amountSpecified: amountOut != 0 ? int256(amountOut) : -int256(amountIn),
64+
sqrtPriceLimitX96: zeroForOne ? MIN_PRICE_LIMIT : MAX_PRICE_LIMIT
65+
}),
66+
hookData
67+
);
68+
69+
// observe deltas
70+
int256 delta0 = poolManager.currencyDelta(address(this), key.currency0);
71+
int256 delta1 = poolManager.currencyDelta(address(this), key.currency1);
72+
73+
// take the output
74+
if (delta0 > 0) key.currency0.take(poolManager, sender, uint256(delta0), false);
75+
if (delta1 > 0) key.currency1.take(poolManager, sender, uint256(delta1), false);
76+
77+
// account for prepaid input against the observed deltas
78+
BalanceDelta returnDelta = toBalanceDelta(int128(delta0), int128(delta1))
79+
+ toBalanceDelta(
80+
zeroForOne ? -int128(int256(amountIn)) : int128(0), zeroForOne ? int128(0) : -int128(int256(amountIn))
81+
);
82+
83+
return abi.encode(returnDelta);
84+
}
85+
}

0 commit comments

Comments
 (0)