Skip to content

Commit 756f79e

Browse files
authored
Merge pull request #61 from euler-xyz/sauce/protocol-fee-ops
Protocol Fee Ops
2 parents 3f2ff64 + 4334265 commit 756f79e

File tree

7 files changed

+137
-11
lines changed

7 files changed

+137
-11
lines changed

docs/audits/EulerSwapHook_Audit_Scope.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,12 @@ The scope of audit involves a re-audit of EulerSwap, primarily `src/`:
5050

5151
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.
5252

53-
An example `test/utils/MinimalRouter.sol` is provided as an example.
53+
An example `test/utils/MinimalRouter.sol` is provided as an example.
54+
55+
### Invalidated Salts
56+
57+
Uniswap v4 Hooks encode their behaviors within the address, requiring deployers to mine salts for a particular address pattern. Because constructor arguments influence the precomputed address during the salt-finding process, governance may accidentally invalidate a discovered salt by updating the protocol fee.
58+
59+
The EulerSwapFactory passes a protocol fee and protocol fee recipient to a EulerSwap instance (hook). If governance were modify either values between salt-discovery and EulerSwap deployment, the deployment would fail.
60+
61+
This scenario is unlikely to happen as we do not expect protocol fee parameters to change; as well, governance can pre-emptively warn deployers of the parameter change.

src/EulerSwapFactory.sol

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ import {EVCUtil} from "ethereum-vault-connector/utils/EVCUtil.sol";
66
import {GenericFactory} from "evk/GenericFactory/GenericFactory.sol";
77

88
import {EulerSwap} from "./EulerSwap.sol";
9+
import {ProtocolFee} from "./utils/ProtocolFee.sol";
910
import {MetaProxyDeployer} from "./MetaProxyDeployer.sol";
1011

1112
/// @title EulerSwapFactory contract
1213
/// @custom:security-contact [email protected]
1314
/// @author Euler Labs (https://www.eulerlabs.com/)
14-
contract EulerSwapFactory is IEulerSwapFactory, EVCUtil {
15+
contract EulerSwapFactory is IEulerSwapFactory, EVCUtil, ProtocolFee {
1516
/// @dev An array to store all pools addresses.
1617
address[] private allPools;
1718
/// @dev Vaults must be deployed by this factory
@@ -33,7 +34,10 @@ contract EulerSwapFactory is IEulerSwapFactory, EVCUtil {
3334
error InvalidVaultImplementation();
3435
error SliceOutOfBounds();
3536

36-
constructor(address evc, address evkFactory_, address eulerSwapImpl_) EVCUtil(evc) {
37+
constructor(address evc, address evkFactory_, address eulerSwapImpl_, address feeOwner_)
38+
EVCUtil(evc)
39+
ProtocolFee(feeOwner_)
40+
{
3741
evkFactory = evkFactory_;
3842
eulerSwapImpl = eulerSwapImpl_;
3943
}
@@ -51,6 +55,10 @@ contract EulerSwapFactory is IEulerSwapFactory, EVCUtil {
5155

5256
uninstall(params.eulerAccount);
5357

58+
// set protocol fee
59+
params.protocolFee = protocolFee;
60+
params.protocolFeeRecipient = protocolFeeRecipient;
61+
5462
EulerSwap pool = EulerSwap(
5563
MetaProxyDeployer.deployMetaProxy(
5664
eulerSwapImpl, abi.encode(params), keccak256(abi.encode(params.eulerAccount, salt))

src/utils/ProtocolFee.sol

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.27;
3+
4+
import {Owned} from "solmate/src/auth/Owned.sol";
5+
6+
abstract contract ProtocolFee is Owned {
7+
uint256 public protocolFee;
8+
address public protocolFeeRecipient;
9+
10+
error InvalidFee();
11+
12+
constructor(address _feeOwner) Owned(_feeOwner) {}
13+
14+
/// @notice Set the protocol fee, expressed as a percentage of LP fee
15+
/// @param newFee The new protocol fee, in WAD units (0.10e18 = 10%)
16+
function setProtocolFee(uint256 newFee) external onlyOwner {
17+
require(newFee < 1e18, InvalidFee());
18+
protocolFee = newFee;
19+
}
20+
21+
function setProtocolFeeRecipient(address newRecipient) external onlyOwner {
22+
protocolFeeRecipient = newRecipient;
23+
}
24+
}

test/Ctx.t.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ contract CtxTest is EulerSwapTestBase {
1414
}
1515

1616
function test_staticParamSize() public view {
17-
IEulerSwap.Params memory params = getEulerSwapParams(1e18, 1e18, 1e18, 1e18, 0.4e18, 0.85e18, 0);
17+
IEulerSwap.Params memory params = getEulerSwapParams(1e18, 1e18, 1e18, 1e18, 0.4e18, 0.85e18, 0, 0, address(0));
1818
assertEq(abi.encode(params).length, 384);
1919
}
2020
}

test/EulerSwapTestBase.t.sol

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ contract EulerSwapTestBase is EVaultTestBase {
4747
);
4848

4949
eulerSwapImpl = address(new EulerSwap{salt: salt}(address(evc), poolManager_));
50-
eulerSwapFactory = new EulerSwapFactory(address(evc), address(factory), eulerSwapImpl);
50+
eulerSwapFactory = new EulerSwapFactory(address(evc), address(factory), eulerSwapImpl, address(this));
5151
periphery = new EulerSwapPeriphery();
5252
}
5353

@@ -138,10 +138,25 @@ contract EulerSwapTestBase is EVaultTestBase {
138138
uint256 py,
139139
uint256 cx,
140140
uint256 cy
141+
) internal returns (EulerSwap) {
142+
return createEulerSwapFull(reserve0, reserve1, fee, px, py, cx, cy, 0, address(0));
143+
}
144+
145+
function createEulerSwapFull(
146+
uint112 reserve0,
147+
uint112 reserve1,
148+
uint256 fee,
149+
uint256 px,
150+
uint256 py,
151+
uint256 cx,
152+
uint256 cy,
153+
uint256 protocolFee,
154+
address protcoolFeeRecipient
141155
) internal returns (EulerSwap) {
142156
removeInstalledOperator();
143157

144-
IEulerSwap.Params memory params = getEulerSwapParams(reserve0, reserve1, px, py, cx, cy, fee);
158+
IEulerSwap.Params memory params =
159+
getEulerSwapParams(reserve0, reserve1, px, py, cx, cy, fee, protocolFee, protcoolFeeRecipient);
145160
IEulerSwap.InitialState memory initialState =
146161
IEulerSwap.InitialState({currReserve0: reserve0, currReserve1: reserve1});
147162

@@ -167,10 +182,25 @@ contract EulerSwapTestBase is EVaultTestBase {
167182
uint256 py,
168183
uint256 cx,
169184
uint256 cy
185+
) internal returns (EulerSwap) {
186+
return createEulerSwapHookFull(reserve0, reserve1, fee, px, py, cx, cy, 0, address(0));
187+
}
188+
189+
function createEulerSwapHookFull(
190+
uint112 reserve0,
191+
uint112 reserve1,
192+
uint256 fee,
193+
uint256 px,
194+
uint256 py,
195+
uint256 cx,
196+
uint256 cy,
197+
uint256 protocolFee,
198+
address protocolFeeRecipient
170199
) internal returns (EulerSwap) {
171200
removeInstalledOperator();
172201

173-
IEulerSwap.Params memory params = getEulerSwapParams(reserve0, reserve1, px, py, cx, cy, fee);
202+
IEulerSwap.Params memory params =
203+
getEulerSwapParams(reserve0, reserve1, px, py, cx, cy, fee, protocolFee, protocolFeeRecipient);
174204
IEulerSwap.InitialState memory initialState =
175205
IEulerSwap.InitialState({currReserve0: reserve0, currReserve1: reserve1});
176206

@@ -244,7 +274,9 @@ contract EulerSwapTestBase is EVaultTestBase {
244274
uint256 py,
245275
uint256 cx,
246276
uint256 cy,
247-
uint256 fee
277+
uint256 fee,
278+
uint256 protocolFee,
279+
address protocolFeeRecipient
248280
) internal view returns (EulerSwap.Params memory) {
249281
return IEulerSwap.Params({
250282
vault0: address(eTST),
@@ -257,8 +289,8 @@ contract EulerSwapTestBase is EVaultTestBase {
257289
concentrationX: cx,
258290
concentrationY: cy,
259291
fee: fee,
260-
protocolFee: 0,
261-
protocolFeeRecipient: address(0)
292+
protocolFee: protocolFee,
293+
protocolFeeRecipient: protocolFeeRecipient
262294
});
263295
}
264296

test/FactoryTest.t.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ contract FactoryTest is EulerSwapTestBase {
2828
view
2929
returns (IEulerSwap.Params memory poolParams, IEulerSwap.InitialState memory initialState)
3030
{
31-
poolParams = getEulerSwapParams(1e18, 1e18, 1e18, 1e18, 0.4e18, 0.85e18, 0);
31+
poolParams = getEulerSwapParams(1e18, 1e18, 1e18, 1e18, 0.4e18, 0.85e18, 0, 0, address(0));
3232
initialState = IEulerSwap.InitialState({currReserve0: 1e18, currReserve1: 1e18});
3333
}
3434

test/HookFees.t.sol

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
1717
contract HookFeesTest is EulerSwapTestBase {
1818
using StateLibrary for IPoolManager;
1919

20+
address protocolFeeRecipient = makeAddr("protocolFeeRecipient");
21+
2022
EulerSwap public eulerSwap;
2123

2224
IPoolManager public poolManager;
@@ -120,4 +122,56 @@ contract HookFeesTest is EulerSwapTestBase {
120122

121123
assertGt(getHolderNAV(), origNav + int256(amountIn - amountInWithoutFee));
122124
}
125+
126+
function test_protocolFee() public {
127+
// set protocol fee to 10% of the LP fee
128+
uint256 protocolFee = 0.1e18;
129+
130+
eulerSwapFactory.setProtocolFee(protocolFee);
131+
eulerSwapFactory.setProtocolFeeRecipient(protocolFeeRecipient);
132+
133+
// set swap fee to 10 bips and activate the pool
134+
eulerSwap = createEulerSwapHookFull(
135+
60e18, 60e18, 0.001e18, 1e18, 1e18, 0.4e18, 0.85e18, protocolFee, protocolFeeRecipient
136+
);
137+
138+
int256 origNav = getHolderNAV();
139+
(uint112 r0, uint112 r1,) = eulerSwap.getReserves();
140+
141+
uint256 amountIn = 1e18;
142+
uint256 amountInWithoutFee = amountIn - (amountIn * eulerSwap.getParams().fee / 1e18);
143+
uint256 amountOut =
144+
periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), amountIn);
145+
146+
assetTST.mint(anyone, amountIn);
147+
148+
vm.startPrank(anyone);
149+
assetTST.approve(address(minimalRouter), amountIn);
150+
151+
bool zeroForOne = address(assetTST) < address(assetTST2);
152+
BalanceDelta result = minimalRouter.swap(eulerSwap.poolKey(), zeroForOne, amountIn, 0, "");
153+
vm.stopPrank();
154+
155+
assertEq(assetTST.balanceOf(anyone), 0);
156+
assertEq(assetTST2.balanceOf(anyone), amountOut);
157+
158+
assertEq(zeroForOne ? uint256(-int256(result.amount0())) : uint256(-int256(result.amount1())), amountIn);
159+
assertEq(zeroForOne ? uint256(int256(result.amount1())) : uint256(int256(result.amount0())), amountOut);
160+
161+
// assert fees were not added to the reserves
162+
(uint112 r0New, uint112 r1New,) = eulerSwap.getReserves();
163+
if (zeroForOne) {
164+
assertEq(r0New, r0 + amountInWithoutFee);
165+
assertEq(r1New, r1 - amountOut);
166+
} else {
167+
// oneForZero, so the curve received asset1
168+
assertEq(r0New, r0 - amountOut);
169+
assertEq(r1New, r1 + amountInWithoutFee);
170+
}
171+
172+
uint256 protocolFeeCollected = assetTST.balanceOf(protocolFeeRecipient);
173+
assertGt(protocolFeeCollected, 0);
174+
175+
assertGt(getHolderNAV(), origNav + int256(amountIn - amountInWithoutFee) - int256(protocolFeeCollected));
176+
}
123177
}

0 commit comments

Comments
 (0)