Skip to content

Commit fce4e34

Browse files
authored
Merge pull request #48 from euler-xyz/v4-exploration
v4 Hook Exploration
2 parents 4dc0b07 + e7776c0 commit fce4e34

15 files changed

+799
-86
lines changed

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,6 @@
1010
[submodule "lib/ethereum-vault-connector"]
1111
path = lib/ethereum-vault-connector
1212
url = https://github.com/euler-xyz/ethereum-vault-connector
13+
[submodule "lib/v4-periphery"]
14+
path = lib/v4-periphery
15+
url = https://github.com/uniswap/v4-periphery
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# EulerSwapHook Audit
2+
3+
Through a new partnership between Euler Labs and Uniswap Foundation, the teams intend to expose EulerSwap's core logic and mechanisms via a Uniswap v4 Hook interface.
4+
5+
This is primarily done by inheriting `EulerSwap.sol:EulerSwap`, i.e. `EulerSwapHook is EulerSwap, BaseHook`, and implementing a "custom curve" via `beforeSwap`. The implementation will allow integrators, interfaces, and aggregators, to trade on EulerSwap as-if it is any other Uniswap v4 Pool
6+
7+
```solidity
8+
// assuming the EulerSwapHook was instantiated via EulerSwapFactory
9+
PoolKey memory poolKey = PoolKey({
10+
currency0: currency0,
11+
currency1: currency1,
12+
fee: fee,
13+
tickSpacing: 60,
14+
hooks: IHooks(address(eulerSwapHook))
15+
});
16+
17+
minimalRouter.swap(poolKey, zeroForOne, amountIn, 0);
18+
```
19+
20+
21+
## Audit Scope
22+
23+
The scope of audit involves the code-diff introduced by [PR #48](https://github.com/euler-xyz/euler-swap/pull/48/files). **As of Apr 1st, 2025, the diff is subject to change but will be code-complete by the audit start time.**
24+
25+
Major Changes will include:
26+
27+
* Replacing `binarySearch` quoting algorithm with a closed-form formula
28+
* Implementing a protocol fee, as a percentage of LP fees, enacted by governance
29+
30+
As for the files in scope, only files from `src/` should be considered:
31+
32+
```
33+
├── src
34+
│ ├── EulerSwapFactory.sol
35+
│ ├── EulerSwapHook.sol
36+
│ └── utils
37+
│ └── HookMiner.sol
38+
```
39+
40+
## Known Caveats
41+
42+
### Prepaid Inputs
43+
44+
Due to technical requirements, EulerSwapHook must take the input token from PoolManager and deposit it into Euler Vaults. It will appear that EulerSwapHook can only support input sizes of `IERC20.balanceOf(PoolManager)`. However swap routers can pre-emptively send input tokens (from user wallet to PoolManager) prior to calling `poolManager.swap` to get around this limitation.
45+
46+
An example `test/utils/MinimalRouter.sol` is provided as an example.

lib/v4-periphery

Submodule v4-periphery added at 9628c36

remappings.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ evk/=lib/euler-vault-kit/src/
44
ethereum-vault-connector/=lib/ethereum-vault-connector/src/
55
evk-test/=lib/euler-vault-kit/test/
66
permit2/=lib/euler-vault-kit/lib/permit2/
7+
@uniswap/v4-core/=lib/v4-periphery/lib/v4-core/

script/DeployProtocol.s.sol

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pragma solidity ^0.8.0;
44
import {ScriptUtil} from "./ScriptUtil.s.sol";
55
import {EulerSwapFactory} from "../src/EulerSwapFactory.sol";
66
import {EulerSwapPeriphery} from "../src/EulerSwapPeriphery.sol";
7+
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
78

89
/// @title Script to deploy EulerSwapFactory & EulerSwapPeriphery.
910
contract DeployProtocol is ScriptUtil {
@@ -17,11 +18,12 @@ contract DeployProtocol is ScriptUtil {
1718
string memory json = _getJsonFile(inputScriptFileName);
1819

1920
address evc = vm.parseJsonAddress(json, ".evc");
21+
address poolManager = vm.parseJsonAddress(json, ".poolManager");
2022
address factory = vm.parseJsonAddress(json, ".factory");
2123

2224
vm.startBroadcast(deployerAddress);
2325

24-
new EulerSwapFactory(evc, factory);
26+
new EulerSwapFactory(IPoolManager(poolManager), evc, factory);
2527
new EulerSwapPeriphery();
2628

2729
vm.stopBroadcast();

script/json/DeployProtocol_input.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
22
"evc": "0x0C9a3dd6b8F28529d72d7f9cE918D493519EE383",
3+
"poolManager": "0x000000000004444c5dc75cB358380D2e3dE08A90",
34
"factory": "0xF75548aF02f1928CbE9015985D4Fcbf96d728544"
45
}

src/EulerSwapFactory.sol

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
// SPDX-License-Identifier: UNLICENSED
22
pragma solidity ^0.8.27;
33

4+
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
45
import {IEulerSwapFactory, IEulerSwap} from "./interfaces/IEulerSwapFactory.sol";
5-
import {EulerSwap} from "./EulerSwap.sol";
6+
import {EulerSwapHook} from "./EulerSwapHook.sol";
67
import {EVCUtil} from "ethereum-vault-connector/utils/EVCUtil.sol";
78
import {GenericFactory} from "evk/GenericFactory/GenericFactory.sol";
89

@@ -18,6 +19,8 @@ contract EulerSwapFactory is IEulerSwapFactory, EVCUtil {
1819
mapping(address eulerAccount => EulerAccountState state) private eulerAccountState;
1920
mapping(address asset0 => mapping(address asset1 => address[])) private poolMap;
2021

22+
IPoolManager immutable poolManager;
23+
2124
event PoolDeployed(
2225
address indexed asset0,
2326
address indexed asset1,
@@ -42,7 +45,8 @@ contract EulerSwapFactory is IEulerSwapFactory, EVCUtil {
4245
error InvalidVaultImplementation();
4346
error SliceOutOfBounds();
4447

45-
constructor(address evc, address evkFactory_) EVCUtil(evc) {
48+
constructor(IPoolManager _manager, address evc, address evkFactory_) EVCUtil(evc) {
49+
poolManager = _manager;
4650
evkFactory = evkFactory_;
4751
}
4852

@@ -59,11 +63,12 @@ contract EulerSwapFactory is IEulerSwapFactory, EVCUtil {
5963

6064
uninstall(params.eulerAccount);
6165

62-
EulerSwap pool = new EulerSwap{salt: keccak256(abi.encode(params.eulerAccount, salt))}(params, curveParams);
66+
EulerSwapHook pool =
67+
new EulerSwapHook{salt: keccak256(abi.encode(params.eulerAccount, salt))}(poolManager, params, curveParams);
6368

6469
updateEulerAccountState(params.eulerAccount, address(pool));
6570

66-
EulerSwap(pool).activate();
71+
pool.activate();
6772

6873
emit PoolDeployed(
6974
pool.asset0(),
@@ -104,7 +109,9 @@ contract EulerSwapFactory is IEulerSwapFactory, EVCUtil {
104109
address(this),
105110
keccak256(abi.encode(address(poolParams.eulerAccount), salt)),
106111
keccak256(
107-
abi.encodePacked(type(EulerSwap).creationCode, abi.encode(poolParams, curveParams))
112+
abi.encodePacked(
113+
type(EulerSwapHook).creationCode, abi.encode(poolManager, poolParams, curveParams)
114+
)
108115
)
109116
)
110117
)
@@ -214,7 +221,7 @@ contract EulerSwapFactory is IEulerSwapFactory, EVCUtil {
214221
/// @param pool The address of the pool to query
215222
/// @return The addresses of asset0 and asset1 in the pool
216223
function _getAssets(address pool) internal view returns (address, address) {
217-
return (EulerSwap(pool).asset0(), EulerSwap(pool).asset1());
224+
return (IEulerSwap(pool).asset0(), IEulerSwap(pool).asset1());
218225
}
219226

220227
/// @notice Returns a slice of an array of addresses

src/EulerSwapHook.sol

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.27;
3+
4+
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
5+
import {BaseHook} from "v4-periphery/src/utils/BaseHook.sol";
6+
import {PoolKey} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
7+
import {Currency} from "@uniswap/v4-core/src/types/Currency.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 {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol";
11+
import {
12+
BeforeSwapDelta, toBeforeSwapDelta, BeforeSwapDeltaLibrary
13+
} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol";
14+
import {EulerSwap, IEulerSwap, IEVault} from "./EulerSwap.sol";
15+
16+
contract EulerSwapHook is EulerSwap, BaseHook {
17+
using SafeCast for uint256;
18+
19+
PoolKey internal _poolKey;
20+
21+
constructor(IPoolManager _manager, Params memory params, CurveParams memory curveParams)
22+
EulerSwap(params, curveParams)
23+
BaseHook(_manager)
24+
{
25+
address asset0Addr = IEVault(params.vault0).asset();
26+
address asset1Addr = IEVault(params.vault1).asset();
27+
28+
// convert fee in WAD to pips. 0.003e18 / 1e12 = 3000 = 0.30%
29+
uint24 fee = uint24(params.fee / 1e12);
30+
31+
_poolKey = PoolKey({
32+
currency0: Currency.wrap(asset0Addr),
33+
currency1: Currency.wrap(asset1Addr),
34+
fee: fee,
35+
tickSpacing: 60, // TODO: fix arbitrary tick spacing
36+
hooks: IHooks(address(this))
37+
});
38+
39+
// create the pool on v4, using starting price as sqrtPrice(1/1) * Q96
40+
poolManager.initialize(_poolKey, 79228162514264337593543950336);
41+
}
42+
43+
/// @dev Helper function to return the poolKey as its struct type
44+
function poolKey() external view returns (PoolKey memory) {
45+
return _poolKey;
46+
}
47+
48+
function _beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata params, bytes calldata)
49+
internal
50+
override
51+
returns (bytes4, BeforeSwapDelta, uint24)
52+
{
53+
// determine inbound/outbound token based on 0->1 or 1->0 swap
54+
bool zeroForOne = params.zeroForOne;
55+
56+
uint256 amountInWithoutFee;
57+
uint256 amountOut;
58+
BeforeSwapDelta returnDelta;
59+
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+
}
73+
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 ? asset0 : asset1, zeroForOne ? vault0 : vault1);
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+
}
97+
98+
{
99+
uint256 newReserve0 = zeroForOne ? (reserve0 + amountInWithoutFee) : (reserve0 - amountOut);
100+
uint256 newReserve1 = !zeroForOne ? (reserve1 + amountInWithoutFee) : (reserve1 - amountOut);
101+
102+
require(newReserve0 <= type(uint112).max && newReserve1 <= type(uint112).max, Overflow());
103+
require(verify(newReserve0, newReserve1), CurveViolation());
104+
105+
reserve0 = uint112(newReserve0);
106+
reserve1 = uint112(newReserve1);
107+
}
108+
109+
return (BaseHook.beforeSwap.selector, returnDelta, 0);
110+
}
111+
112+
// TODO: fix salt mining & verification for the hook
113+
function getHookPermissions() public pure override returns (Hooks.Permissions memory) {}
114+
function validateHookAddress(BaseHook) internal pure override {}
115+
116+
error SwapLimitExceeded();
117+
error OperatorNotInstalled();
118+
119+
function computeQuote(bool asset0IsInput, uint256 amount, bool exactIn) internal view returns (uint256) {
120+
require(evc.isAccountOperatorAuthorized(eulerAccount, address(this)), OperatorNotInstalled());
121+
require(amount <= type(uint112).max, SwapLimitExceeded());
122+
123+
// exactIn: decrease effective amountIn
124+
if (exactIn) amount = amount - (amount * fee / 1e18);
125+
126+
(uint256 inLimit, uint256 outLimit) = calcLimits(asset0IsInput);
127+
128+
uint256 quote = binarySearch(amount, exactIn, asset0IsInput);
129+
130+
if (exactIn) {
131+
// if `exactIn`, `quote` is the amount of assets to buy from the AMM
132+
require(amount <= inLimit && quote <= outLimit, SwapLimitExceeded());
133+
} else {
134+
// if `!exactIn`, `amount` is the amount of assets to buy from the AMM
135+
require(amount <= outLimit && quote <= inLimit, SwapLimitExceeded());
136+
}
137+
138+
// exactOut: inflate required amountIn
139+
if (!exactIn) quote = (quote * 1e18) / (1e18 - fee);
140+
141+
return quote;
142+
}
143+
144+
function binarySearch(uint256 amount, bool exactIn, bool asset0IsInput) internal view returns (uint256 output) {
145+
int256 dx;
146+
int256 dy;
147+
148+
if (exactIn) {
149+
if (asset0IsInput) dx = int256(amount);
150+
else dy = int256(amount);
151+
} else {
152+
if (asset0IsInput) dy = -int256(amount);
153+
else dx = -int256(amount);
154+
}
155+
156+
unchecked {
157+
int256 reserve0New = int256(uint256(reserve0)) + dx;
158+
int256 reserve1New = int256(uint256(reserve1)) + dy;
159+
require(reserve0New > 0 && reserve1New > 0, SwapLimitExceeded());
160+
161+
uint256 low;
162+
uint256 high = type(uint112).max;
163+
164+
while (low < high) {
165+
uint256 mid = (low + high) / 2;
166+
require(mid > 0, SwapLimitExceeded());
167+
(uint256 a, uint256 b) = dy == 0 ? (uint256(reserve0New), mid) : (mid, uint256(reserve1New));
168+
if (verify(a, b)) {
169+
high = mid;
170+
} else {
171+
low = mid + 1;
172+
}
173+
}
174+
175+
require(high < type(uint112).max, SwapLimitExceeded()); // at least one point verified
176+
177+
if (dx != 0) dy = int256(low) - reserve1New;
178+
else dx = int256(low) - reserve0New;
179+
}
180+
181+
if (exactIn) {
182+
if (asset0IsInput) output = uint256(-dy);
183+
else output = uint256(-dx);
184+
} else {
185+
if (asset0IsInput) output = dx >= 0 ? uint256(dx) : 0;
186+
else output = dy >= 0 ? uint256(dy) : 0;
187+
}
188+
}
189+
190+
function calcLimits(bool asset0IsInput) internal view returns (uint256, uint256) {
191+
uint256 inLimit = type(uint112).max;
192+
uint256 outLimit = type(uint112).max;
193+
194+
(IEVault vault0, IEVault vault1) = (IEVault(vault0), IEVault(vault1));
195+
// Supply caps on input
196+
{
197+
IEVault vault = (asset0IsInput ? vault0 : vault1);
198+
uint256 maxDeposit = vault.debtOf(eulerAccount) + vault.maxDeposit(eulerAccount);
199+
if (maxDeposit < inLimit) inLimit = maxDeposit;
200+
}
201+
202+
// Remaining reserves of output
203+
{
204+
uint112 reserveLimit = asset0IsInput ? reserve1 : reserve0;
205+
if (reserveLimit < outLimit) outLimit = reserveLimit;
206+
}
207+
208+
// Remaining cash and borrow caps in output
209+
{
210+
IEVault vault = (asset0IsInput ? vault1 : vault0);
211+
212+
uint256 cash = vault.cash();
213+
if (cash < outLimit) outLimit = cash;
214+
215+
(, uint16 borrowCap) = vault.caps();
216+
uint256 maxWithdraw = decodeCap(uint256(borrowCap));
217+
maxWithdraw = vault.totalBorrows() > maxWithdraw ? 0 : maxWithdraw - vault.totalBorrows();
218+
if (maxWithdraw > cash) maxWithdraw = cash;
219+
maxWithdraw += vault.convertToAssets(vault.balanceOf(eulerAccount));
220+
if (maxWithdraw < outLimit) outLimit = maxWithdraw;
221+
}
222+
223+
return (inLimit, outLimit);
224+
}
225+
226+
function decodeCap(uint256 amountCap) internal pure returns (uint256) {
227+
if (amountCap == 0) return type(uint256).max;
228+
229+
unchecked {
230+
// Cannot overflow because this is less than 2**256:
231+
// 10**(2**6 - 1) * (2**10 - 1) = 1.023e+66
232+
return 10 ** (amountCap & 63) * (amountCap >> 6) / 100;
233+
}
234+
}
235+
}

0 commit comments

Comments
 (0)