Skip to content

Commit 68197e0

Browse files
committed
reworked router to prepay input
1 parent 95e473f commit 68197e0

File tree

2 files changed

+76
-31
lines changed

2 files changed

+76
-31
lines changed

test/EulerSwapHook.swaps.t.sol

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {MinimalRouter} from "./utils/MinimalRouter.sol";
1212
import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol";
1313
import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
1414
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
15+
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
1516

1617
contract EulerSwapHookTest is EulerSwapTestBase {
1718
using StateLibrary for IPoolManager;
@@ -38,14 +39,10 @@ contract EulerSwapHookTest is EulerSwapTestBase {
3839
assertFalse(eulerSwap.poolKey().currency1 == CurrencyLibrary.ADDRESS_ZERO);
3940
(uint160 sqrtPriceX96,,,) = poolManager.getSlot0(eulerSwap.poolKey().toId());
4041
assertNotEq(sqrtPriceX96, 0);
41-
42-
// Seed the poolManager with balance so that transient withdrawing before depositing succeeds
43-
assetTST.mint(address(poolManager), 1000e18);
44-
assetTST2.mint(address(poolManager), 1000e18);
4542
}
4643

4744
function test_SwapExactIn() public {
48-
uint256 amountIn = 1001e18;
45+
uint256 amountIn = 1e18;
4946
uint256 amountOut =
5047
periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), amountIn);
5148

@@ -55,17 +52,20 @@ contract EulerSwapHookTest is EulerSwapTestBase {
5552
assetTST.approve(address(minimalRouter), amountIn);
5653

5754
bool zeroForOne = address(assetTST) < address(assetTST2);
58-
minimalRouter.swap(eulerSwap.poolKey(), zeroForOne, true, amountIn, "");
55+
BalanceDelta result = minimalRouter.swap(eulerSwap.poolKey(), zeroForOne, amountIn, 0, "");
5956
vm.stopPrank();
6057

6158
assertEq(assetTST.balanceOf(anyone), 0);
6259
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);
6363
}
6464

6565
/// @dev swapping with an amount that exceeds PoolManager's ERC20 token balance will revert
6666
/// if the router does not pre-pay the input
6767
function test_swapExactIn_revertWithoutTokenLiquidity() public {
68-
uint256 amountIn = 1001e18; // input amount exceeds
68+
uint256 amountIn = 1e18; // input amount exceeds PoolManager balance
6969

7070
assetTST.mint(anyone, amountIn);
7171

@@ -87,13 +87,35 @@ contract EulerSwapHookTest is EulerSwapTestBase {
8787
assetTST.mint(anyone, amountIn);
8888

8989
vm.startPrank(anyone);
90-
assetTST.approve(address(swapRouter), amountIn);
90+
assetTST.approve(address(minimalRouter), amountIn);
91+
9192
bool zeroForOne = address(assetTST) < address(assetTST2);
92-
_swap(eulerSwap.poolKey(), zeroForOne, false, amountOut);
93+
BalanceDelta result = minimalRouter.swap(eulerSwap.poolKey(), zeroForOne, amountIn, amountOut, "");
9394
vm.stopPrank();
9495

9596
assertEq(assetTST.balanceOf(anyone), 0);
9697
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();
97119
}
98120

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

test/utils/MinimalRouter.sol

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,68 +3,91 @@ pragma solidity ^0.8.24;
33

44
import {CurrencyLibrary, Currency} from "@uniswap/v4-core/src/types/Currency.sol";
55
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
6-
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
6+
import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
77
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
88
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
99
import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
1010
import {CurrencySettler} from "@uniswap/v4-core/test/utils/CurrencySettler.sol";
1111
import {SafeCallback} from "v4-periphery/src/base/SafeCallback.sol";
1212
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
13+
import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol";
1314

1415
contract MinimalRouter is SafeCallback {
16+
using TransientStateLibrary for IPoolManager;
1517
using CurrencySettler for Currency;
1618

1719
uint160 public constant MIN_PRICE_LIMIT = TickMath.MIN_SQRT_PRICE + 1;
1820
uint160 public constant MAX_PRICE_LIMIT = TickMath.MAX_SQRT_PRICE - 1;
1921

2022
constructor(IPoolManager _manager) SafeCallback(_manager) {}
2123

22-
function swap(PoolKey memory key, bool zeroForOne, bool exactInput, uint256 amount, bytes memory hookData)
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)
2331
external
2432
payable
2533
returns (BalanceDelta delta)
2634
{
2735
delta = abi.decode(
28-
poolManager.unlock(abi.encode(msg.sender, key, zeroForOne, exactInput, amount, hookData)), (BalanceDelta)
36+
poolManager.unlock(abi.encode(msg.sender, key, zeroForOne, amountIn, amountOut, hookData)), (BalanceDelta)
2937
);
3038

3139
uint256 ethBalance = address(this).balance;
3240
if (ethBalance > 0) CurrencyLibrary.ADDRESS_ZERO.transfer(msg.sender, ethBalance);
3341
}
3442

3543
function _unlockCallback(bytes calldata data) internal override returns (bytes memory) {
36-
(address sender, PoolKey memory key, bool zeroForOne, bool exactInput, uint256 amount, bytes memory hookData) =
37-
abi.decode(data, (address, PoolKey, bool, bool, uint256, bytes));
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));
3852

39-
// for exact input swaps, send the input first to avoid PoolManager token balance issues
40-
if (exactInput) {
41-
zeroForOne
42-
? key.currency0.settle(poolManager, sender, amount, false)
43-
: key.currency1.settle(poolManager, sender, amount, false);
44-
}
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);
4557

46-
BalanceDelta delta = poolManager.swap(
58+
poolManager.swap(
4759
key,
4860
IPoolManager.SwapParams({
4961
zeroForOne: zeroForOne,
50-
amountSpecified: exactInput ? -int256(amount) : int256(amount),
62+
amountSpecified: amountOut != 0 ? int256(amountOut) : -int256(amountIn),
5163
sqrtPriceLimitX96: zeroForOne ? MIN_PRICE_LIMIT : MAX_PRICE_LIMIT
5264
}),
5365
hookData
5466
);
5567

56-
if (!exactInput && delta.amount0() < 0) {
57-
key.currency0.settle(poolManager, sender, uint256(int256(-delta.amount0())), false);
58-
} else if (delta.amount0() > 0) {
59-
key.currency0.take(poolManager, sender, uint256(int256(delta.amount0())), false);
68+
// observe deltas
69+
int256 delta0 = poolManager.currencyDelta(address(this), key.currency0);
70+
int256 delta1 = poolManager.currencyDelta(address(this), key.currency1);
71+
72+
// primarily take the output token, and excess amounts for exact output swaps
73+
if (delta0 < 0) {
74+
key.currency0.settle(poolManager, sender, uint256(-delta0), false);
75+
} else if (delta0 > 0) {
76+
key.currency0.take(poolManager, sender, uint256(delta0), false);
6077
}
6178

62-
if (!exactInput && delta.amount1() < 0) {
63-
key.currency1.settle(poolManager, sender, uint256(int256(-delta.amount1())), false);
64-
} else if (delta.amount1() > 0) {
65-
key.currency1.take(poolManager, sender, uint256(int256(delta.amount1())), false);
79+
if (delta1 < 0) {
80+
key.currency1.settle(poolManager, sender, uint256(-delta1), false);
81+
} else if (delta1 > 0) {
82+
key.currency1.take(poolManager, sender, uint256(delta1), false);
6683
}
6784

68-
return abi.encode(delta);
85+
// account for prepaid input against the observed deltas
86+
BalanceDelta returnDelta = toBalanceDelta(int128(delta0), int128(delta1))
87+
+ toBalanceDelta(
88+
zeroForOne ? -int128(int256(amountIn)) : int128(0), zeroForOne ? int128(0) : -int128(int256(amountIn))
89+
);
90+
91+
return abi.encode(returnDelta);
6992
}
7093
}

0 commit comments

Comments
 (0)