Skip to content

Commit 22bb23a

Browse files
committed
wrap errors thrown from hooks in HookError(), make this error challengeable
Spearbit #3
1 parent 70cd473 commit 22bb23a

File tree

6 files changed

+231
-24
lines changed

6 files changed

+231
-24
lines changed

src/EulerSwapRegistry.sol

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {IEulerSwap} from "./interfaces/IEulerSwap.sol";
88
import {IEulerSwapFactory} from "./interfaces/IEulerSwapFactory.sol";
99
import {IEulerSwapRegistry} from "./interfaces/IEulerSwapRegistry.sol";
1010
import {IPerspective} from "./interfaces/IPerspective.sol";
11+
import {SwapLib} from "./libraries/SwapLib.sol";
1112
import {EVCUtil} from "ethereum-vault-connector/utils/EVCUtil.sol";
1213

1314
/// @title EulerSwapRegistry contract
@@ -200,7 +201,10 @@ contract EulerSwapRegistry is IEulerSwapRegistry, EVCUtil {
200201
)
201202
);
202203
require(!success, ChallengeSwapSucceeded());
203-
require(bytes4(error) == E_AccountLiquidity.selector, ChallengeSwapNotLiquidityFailure());
204+
require(
205+
bytes4(error) == E_AccountLiquidity.selector || bytes4(error) == SwapLib.HookError.selector,
206+
ChallengeSwapNotLiquidityFailure()
207+
);
204208
}
205209

206210
uint256 bondAmount = validityBonds[poolAddr];

src/libraries/QuoteLib.sol

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {IEulerSwap} from "../interfaces/IEulerSwap.sol";
77
import "../interfaces/IEulerSwapHookTarget.sol";
88
import {CtxLib} from "./CtxLib.sol";
99
import {CurveLib} from "./CurveLib.sol";
10+
import {SwapLib} from "./SwapLib.sol";
1011

1112
library QuoteLib {
1213
error HookError();
@@ -22,7 +23,11 @@ library QuoteLib {
2223
if ((dParams.swapHookedOperations & EULER_SWAP_HOOK_GET_FEE) != 0) {
2324
CtxLib.State storage s = CtxLib.getState();
2425

25-
fee = IEulerSwapHookTarget(dParams.swapHook).getFee(asset0IsInput, s.reserve0, s.reserve1, false);
26+
(bool success, bytes memory data) = dParams.swapHook.call(
27+
abi.encodeCall(IEulerSwapHookTarget.getFee, (asset0IsInput, s.reserve0, s.reserve1, false))
28+
);
29+
require(success && data.length >= 32, SwapLib.HookError(EULER_SWAP_HOOK_GET_FEE, data));
30+
fee = abi.decode(data, (uint64));
2631
}
2732

2833
if (fee == type(uint64).max) fee = asset0IsInput ? dParams.fee0 : dParams.fee1;
@@ -41,7 +46,7 @@ library QuoteLib {
4146
(bool success, bytes memory data) = dParams.swapHook.staticcall(
4247
abi.encodeCall(IEulerSwapHookTarget.getFee, (asset0IsInput, s.reserve0, s.reserve1, true))
4348
);
44-
require(success && data.length >= 32, HookError());
49+
require(success && data.length >= 32, SwapLib.HookError(EULER_SWAP_HOOK_GET_FEE, data));
4550
fee = abi.decode(data, (uint64));
4651
}
4752

src/libraries/SwapLib.sol

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ library SwapLib {
3535
);
3636

3737
error CurveViolation();
38+
error HookError(uint8 hookFlag, bytes wrappedError);
3839

3940
struct SwapContext {
4041
// Populated by init
@@ -79,9 +80,39 @@ library SwapLib {
7980
}
8081

8182
function invokeBeforeSwapHook(SwapContext memory ctx) internal {
82-
if ((ctx.dParams.swapHookedOperations & EULER_SWAP_HOOK_BEFORE_SWAP) != 0) {
83-
IEulerSwapHookTarget(ctx.dParams.swapHook).beforeSwap(ctx.amount0Out, ctx.amount1Out, ctx.sender, ctx.to);
84-
}
83+
if ((ctx.dParams.swapHookedOperations & EULER_SWAP_HOOK_BEFORE_SWAP) == 0) return;
84+
85+
(bool success, bytes memory data) = ctx.dParams.swapHook.call(
86+
abi.encodeCall(IEulerSwapHookTarget.beforeSwap, (ctx.amount0Out, ctx.amount1Out, ctx.sender, ctx.to))
87+
);
88+
require(success, HookError(EULER_SWAP_HOOK_BEFORE_SWAP, data));
89+
}
90+
91+
function invokeAfterSwapHook(SwapContext memory ctx, CtxLib.State storage s, uint256 fee0, uint256 fee1) internal {
92+
if ((ctx.dParams.swapHookedOperations & EULER_SWAP_HOOK_AFTER_SWAP) == 0) return;
93+
94+
s.status = 1; // Unlock the reentrancy guard during afterSwap, allowing hook to reconfigure()
95+
96+
(bool success, bytes memory data) = ctx.dParams.swapHook.call(
97+
abi.encodeCall(
98+
IEulerSwapHookTarget.afterSwap,
99+
(
100+
ctx.amount0In,
101+
ctx.amount1In,
102+
ctx.amount0Out,
103+
ctx.amount1Out,
104+
fee0,
105+
fee1,
106+
ctx.sender,
107+
ctx.to,
108+
s.reserve0,
109+
s.reserve1
110+
)
111+
)
112+
);
113+
require(success, HookError(EULER_SWAP_HOOK_AFTER_SWAP, data));
114+
115+
s.status = 2;
85116
}
86117

87118
function doDeposits(SwapContext memory ctx) internal {
@@ -121,24 +152,7 @@ library SwapLib {
121152
ctx.to
122153
);
123154

124-
if ((ctx.dParams.swapHookedOperations & EULER_SWAP_HOOK_AFTER_SWAP) != 0) {
125-
s.status = 1; // Unlock the reentrancy guard during afterSwap, allowing hook to reconfigure()
126-
127-
IEulerSwapHookTarget(ctx.dParams.swapHook).afterSwap(
128-
ctx.amount0In,
129-
ctx.amount1In,
130-
ctx.amount0Out,
131-
ctx.amount1Out,
132-
fee0,
133-
fee1,
134-
ctx.sender,
135-
ctx.to,
136-
s.reserve0,
137-
s.reserve1
138-
);
139-
140-
s.status = 2;
141-
}
155+
invokeAfterSwapHook(ctx, s, fee0, fee1);
142156
}
143157

144158
// Private

test/Challenge.t.sol

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
pragma solidity ^0.8.24;
33

44
import {IEVault, IEulerSwap, EulerSwapTestBase, EulerSwap, TestERC20} from "./EulerSwapTestBase.t.sol";
5+
import {EulerSwapRegistry} from "../src/EulerSwapRegistry.sol";
6+
import "../src/interfaces/IEulerSwapHookTarget.sol";
7+
import "../src/libraries/SwapLib.sol";
8+
import "evk/EVault/shared/lib/RevertBytes.sol";
59

610
contract ChallengeTest is EulerSwapTestBase {
711
EulerSwap public eulerSwap;
@@ -97,4 +101,120 @@ contract ChallengeTest is EulerSwapTestBase {
97101

98102
assertEq(holder.balance, 0.123e18);
99103
}
104+
105+
function test_challengeHookRevert() public {
106+
vm.deal(holder, 0.123e18);
107+
eulerSwap = createEulerSwap(10e18, 10e18, 0, 1e18, 1e18, 0.9999e18, 0.9999e18);
108+
109+
uint256 amountIn = 5e18;
110+
uint256 amountOut =
111+
periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), amountIn);
112+
113+
// Plain swap is OK:
114+
115+
{
116+
uint256 snapshot = vm.snapshotState();
117+
118+
assetTST.mint(address(this), amountIn);
119+
assetTST.transfer(address(eulerSwap), amountIn);
120+
121+
eulerSwap.swap(0, amountOut, address(this), "");
122+
123+
vm.revertToState(snapshot);
124+
}
125+
126+
// Reconfigure to have a beforeSwap hook that fails
127+
128+
setHook(EULER_SWAP_HOOK_BEFORE_SWAP, 0, 0);
129+
130+
{
131+
uint256 snapshot = vm.snapshotState();
132+
133+
assetTST.mint(address(this), amountIn);
134+
assetTST.transfer(address(eulerSwap), amountIn);
135+
136+
vm.expectRevert(
137+
abi.encodeWithSelector(
138+
SwapLib.HookError.selector, EULER_SWAP_HOOK_BEFORE_SWAP, bytes("not gonna happen")
139+
)
140+
);
141+
eulerSwap.swap(0, amountOut, address(this), "");
142+
143+
vm.revertToState(snapshot);
144+
}
145+
146+
// Non-swap errors are not challengeable, for example insufficient input tokens:
147+
148+
vm.expectRevert(EulerSwapRegistry.ChallengeSwapNotLiquidityFailure.selector);
149+
eulerSwapRegistry.challengePool(
150+
address(eulerSwap), address(assetTST), address(assetTST2), amountIn, true, address(5555)
151+
);
152+
153+
// Give the input tokens and challenge it:
154+
155+
assetTST.mint(address(this), amountIn); // challenge funds
156+
assetTST.approve(address(eulerSwapRegistry), amountIn);
157+
158+
assertEq(eulerSwapRegistry.poolsLength(), 1);
159+
160+
eulerSwapRegistry.challengePool(
161+
address(eulerSwap), address(assetTST), address(assetTST2), amountIn, true, address(5555)
162+
);
163+
164+
assertEq(eulerSwapRegistry.poolsLength(), 0); // removed from lists
165+
}
166+
167+
function test_challengeHookRevert2() public {
168+
vm.deal(holder, 0.123e18);
169+
eulerSwap = createEulerSwap(10e18, 10e18, 0, 1e18, 1e18, 0.9999e18, 0.9999e18);
170+
171+
uint256 amountIn = 5e18;
172+
uint256 amountOut =
173+
periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), amountIn);
174+
175+
// Plain swap is OK:
176+
177+
{
178+
uint256 snapshot = vm.snapshotState();
179+
180+
assetTST.mint(address(this), amountIn);
181+
assetTST.transfer(address(eulerSwap), amountIn);
182+
183+
eulerSwap.swap(0, amountOut, address(this), "");
184+
185+
vm.revertToState(snapshot);
186+
}
187+
188+
// Reconfigure so output asset transfers fail
189+
190+
assetTST.mint(address(this), amountIn);
191+
assetTST.transfer(address(eulerSwap), amountIn);
192+
193+
assetTST2.configure("transfer/revert", bytes("0"));
194+
195+
vm.expectRevert(bytes("revert behaviour"));
196+
eulerSwap.swap(0, amountOut, address(this), "");
197+
198+
// But this error is not challengeable
199+
200+
vm.expectRevert(EulerSwapRegistry.ChallengeSwapNotLiquidityFailure.selector);
201+
eulerSwapRegistry.challengePool(
202+
address(eulerSwap), address(assetTST), address(assetTST2), amountIn, true, address(5555)
203+
);
204+
}
205+
206+
function beforeSwap(uint256, uint256, address, address) external pure {
207+
RevertBytes.revertBytes("not gonna happen");
208+
}
209+
210+
function setHook(uint8 hookedOps, uint64 fee0Param, uint64 fee1Param) internal {
211+
PoolConfig memory pc = getPoolConfig(eulerSwap);
212+
213+
pc.dParams.fee0 = fee0Param;
214+
pc.dParams.fee1 = fee1Param;
215+
pc.dParams.swapHookedOperations = hookedOps;
216+
pc.dParams.swapHook = address(this);
217+
218+
reconfigurePool(eulerSwap, pc);
219+
}
100220
}

test/EulerSwapHooks.t.sol

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {IEVault, IEulerSwap, EulerSwapTestBase, EulerSwap, TestERC20} from "./Eu
55
import {QuoteLib} from "../src/libraries/QuoteLib.sol";
66
import {SwapLib} from "../src/libraries/SwapLib.sol";
77
import "../src/interfaces/IEulerSwapHookTarget.sol";
8+
import "evk/EVault/shared/lib/RevertBytes.sol";
89

910
contract EulerSwapHooks is EulerSwapTestBase {
1011
EulerSwap public eulerSwap;
@@ -18,6 +19,7 @@ contract EulerSwapHooks is EulerSwapTestBase {
1819
uint64 fee0 = 0;
1920
uint64 fee1 = 0;
2021
bool expectRejectedError = false;
22+
bytes expectSwapError;
2123
address toOverride;
2224
address swapHookOverride;
2325
bool setSwapHookOverride = false;
@@ -39,12 +41,15 @@ contract EulerSwapHooks is EulerSwapTestBase {
3941

4042
uint64 beforeSwapCounter = 0;
4143

44+
bytes bs_throwError;
4245
uint256 bs_amount0Out;
4346
uint256 bs_amount1Out;
4447
address bs_msgSender;
4548
address bs_to;
4649

4750
function beforeSwap(uint256 amount0Out, uint256 amount1Out, address msgSender, address to) external {
51+
if (bs_throwError.length > 0) RevertBytes.revertBytes(bs_throwError);
52+
4853
beforeSwapCounter++;
4954

5055
bs_amount0Out = amount0Out;
@@ -53,6 +58,7 @@ contract EulerSwapHooks is EulerSwapTestBase {
5358
bs_to = to;
5459
}
5560

61+
bytes gf_throwError;
5662
uint64 getFeeCounter = 0;
5763
uint112 gf_reserve0;
5864
uint112 gf_reserve1;
@@ -62,6 +68,8 @@ contract EulerSwapHooks is EulerSwapTestBase {
6268
returns (uint64 fee)
6369
{
6470
if (!readOnly) {
71+
if (gf_throwError.length > 0) RevertBytes.revertBytes(gf_throwError);
72+
6573
getFeeCounter++;
6674

6775
gf_reserve0 = reserve0;
@@ -85,6 +93,7 @@ contract EulerSwapHooks is EulerSwapTestBase {
8593
uint256 as_reserve1;
8694

8795
uint64 as_reconfigure_fee0;
96+
uint8 as_reconfigure_swapHookedOperations;
8897

8998
function afterSwap(
9099
uint256 amount0In,
@@ -125,6 +134,16 @@ contract EulerSwapHooks is EulerSwapTestBase {
125134
// called from hook, not eulerAccount!
126135
eulerSwap.reconfigure(p, initial);
127136
}
137+
138+
if (as_reconfigure_swapHookedOperations != 0) {
139+
EulerSwap.InitialState memory initial;
140+
(initial.reserve0, initial.reserve1,) = eulerSwap.getReserves(); // confirms re-entrancy lock released
141+
142+
EulerSwap.DynamicParams memory p = eulerSwap.getDynamicParams();
143+
144+
p.swapHookedOperations = as_reconfigure_swapHookedOperations;
145+
eulerSwap.reconfigure(p, initial);
146+
}
128147
}
129148

130149
function doSwap(bool exactIn, TestERC20 assetIn, TestERC20 assetOut, uint256 amount, uint256 expectedAmount)
@@ -149,6 +168,7 @@ contract EulerSwapHooks is EulerSwapTestBase {
149168
assetIn.transfer(address(eulerSwap), amountIn);
150169

151170
if (expectRejectedError) vm.expectRevert(QuoteLib.SwapRejected.selector);
171+
if (expectSwapError.length > 0) vm.expectRevert(expectSwapError);
152172

153173
address to = toOverride == address(0) ? address(this) : toOverride;
154174

@@ -158,6 +178,8 @@ contract EulerSwapHooks is EulerSwapTestBase {
158178
eulerSwap.swap(amountOut, 0, to, "");
159179
}
160180

181+
if (expectRejectedError || expectSwapError.length > 0) return;
182+
161183
assertEq(assetOut.balanceOf(to), amountOut);
162184
}
163185

@@ -220,6 +242,14 @@ contract EulerSwapHooks is EulerSwapTestBase {
220242
assertEq(bs_to, toOverride);
221243
}
222244

245+
function test_beforeSwapError() public {
246+
setHook(EULER_SWAP_HOOK_BEFORE_SWAP, 0, 0);
247+
bs_throwError = bytes("oops!");
248+
249+
expectSwapError = abi.encodeWithSelector(SwapLib.HookError.selector, EULER_SWAP_HOOK_BEFORE_SWAP, bs_throwError);
250+
doSwap(true, assetTST, assetTST2, 1e18, 0.9974e18);
251+
}
252+
223253
// getFee hook
224254

225255
function test_getFeeHook1() public {
@@ -255,6 +285,14 @@ contract EulerSwapHooks is EulerSwapTestBase {
255285
doSwap(true, assetTST2, assetTST, 1e18, 0.9875e18); // 1% fee
256286
}
257287

288+
function test_getFeeHookError() public {
289+
setHook(EULER_SWAP_HOOK_GET_FEE, 0, 0);
290+
gf_throwError = bytes("oh no!");
291+
292+
expectSwapError = abi.encodeWithSelector(SwapLib.HookError.selector, EULER_SWAP_HOOK_GET_FEE, gf_throwError);
293+
doSwap(true, assetTST, assetTST2, 1e18, 0.9974e18);
294+
}
295+
258296
// Hooks, but hook returns sentinel value, indicating to use default fee
259297

260298
function test_getFeeHookDefault1() public {
@@ -335,6 +373,18 @@ contract EulerSwapHooks is EulerSwapTestBase {
335373
assertEq(p.fee0, 0.077e18);
336374
}
337375

376+
function test_afterSwapReconfigureError() public {
377+
setHook(EULER_SWAP_HOOK_AFTER_SWAP, 0, 0);
378+
as_reconfigure_swapHookedOperations = 100;
379+
380+
expectSwapError = abi.encodeWithSelector(
381+
SwapLib.HookError.selector,
382+
EULER_SWAP_HOOK_AFTER_SWAP,
383+
abi.encodeWithSelector(EulerSwap.BadDynamicParam.selector)
384+
);
385+
doSwap(true, assetTST, assetTST2, 1e18, 0.9974e18);
386+
}
387+
338388
// Multiple hooks
339389

340390
function test_multipleHooks1() public {

0 commit comments

Comments
 (0)