Skip to content

Commit b3dfd85

Browse files
committed
prevent griefing where attacker front-runs dust transfer to trigger E_ZeroShares
1 parent c4de78d commit b3dfd85

File tree

2 files changed

+100
-10
lines changed

2 files changed

+100
-10
lines changed

src/EulerSwap.sol

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pragma solidity ^0.8.27;
33

44
import {IEVC} from "evc/interfaces/IEthereumVaultConnector.sol";
55
import {IEVault, IERC20, IBorrowing, IERC4626, IRiskManager} from "evk/EVault/IEVault.sol";
6+
import {Errors as EVKErrors} from "evk/EVault/shared/Errors.sol";
67
import {IUniswapV2Callee} from "./interfaces/IUniswapV2Callee.sol";
78
import {IEulerSwap} from "./interfaces/IEulerSwap.sol";
89
import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol";
@@ -50,6 +51,7 @@ contract EulerSwap is IEulerSwap, EVCUtil {
5051
error DifferentEVC();
5152
error AssetsOutOfOrderOrEqual();
5253
error CurveViolation();
54+
error DepositFailure(bytes reason);
5355

5456
modifier nonReentrant() {
5557
if (status == 0) activate();
@@ -108,16 +110,10 @@ contract EulerSwap is IEulerSwap, EVCUtil {
108110
// Deposit all available funds, adjust received amounts downward to collect fees
109111

110112
uint256 amount0In = IERC20(asset0).balanceOf(address(this));
111-
if (amount0In > 0) {
112-
depositAssets(vault0, amount0In);
113-
amount0In = amount0In * feeMultiplier / 1e18;
114-
}
113+
if (amount0In > 0) amount0In = depositAssets(vault0, amount0In) * feeMultiplier / 1e18;
115114

116115
uint256 amount1In = IERC20(asset1).balanceOf(address(this));
117-
if (amount1In > 0) {
118-
depositAssets(vault1, amount1In);
119-
amount1In = amount1In * feeMultiplier / 1e18;
120-
}
116+
if (amount1In > 0) amount1In = depositAssets(vault1, amount1In) * feeMultiplier / 1e18;
121117

122118
// Verify curve invariant is satisified
123119

@@ -199,8 +195,12 @@ contract EulerSwap is IEulerSwap, EVCUtil {
199195
}
200196
}
201197

202-
function depositAssets(address vault, uint256 amount) internal {
203-
IEVault(vault).deposit(amount, myAccount);
198+
function depositAssets(address vault, uint256 amount) internal returns (uint256) {
199+
try IEVault(vault).deposit(amount, myAccount) {}
200+
catch (bytes memory reason) {
201+
require(bytes4(reason) == EVKErrors.E_ZeroShares.selector, DepositFailure(reason));
202+
return 0;
203+
}
204204

205205
if (IEVC(evc).isControllerEnabled(myAccount, vault)) {
206206
IEVC(evc).call(
@@ -211,6 +211,8 @@ contract EulerSwap is IEulerSwap, EVCUtil {
211211
IEVC(evc).call(vault, myAccount, 0, abi.encodeCall(IRiskManager.disableController, ()));
212212
}
213213
}
214+
215+
return amount;
214216
}
215217

216218
function myDebt(address vault) internal view returns (uint256) {

test/DepositFailures.t.sol

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
pragma solidity ^0.8.24;
3+
4+
import {IEVault, IEulerSwap, EulerSwapTestBase, EulerSwap, TestERC20} from "./EulerSwapTestBase.t.sol";
5+
import {IRMTestFixed} from "evk-test/mocks/IRMTestFixed.sol";
6+
import {Errors as EVKErrors} from "evk/EVault/shared/Errors.sol";
7+
import "evk/EVault/shared/Constants.sol" as EVKConstants;
8+
9+
contract DepositFailuresTest is EulerSwapTestBase {
10+
EulerSwap public eulerSwap;
11+
address public griefer = makeAddr("griefer");
12+
13+
function setUp() public virtual override {
14+
super.setUp();
15+
16+
eulerSwap = createEulerSwap(50e18, 50e18, 0, 1e18, 1e18, 0.4e18, 0.85e18);
17+
}
18+
19+
function test_griefing() public monotonicHolderNAV {
20+
// Make a borrow to push exchange rate > 1
21+
22+
eTST2.setInterestRateModel(address(new IRMTestFixed()));
23+
24+
mintAndDeposit(griefer, eTST, 100e18);
25+
26+
vm.prank(griefer);
27+
evc.enableCollateral(griefer, address(eTST));
28+
vm.prank(griefer);
29+
evc.enableController(griefer, address(eTST2));
30+
31+
vm.prank(griefer);
32+
eTST2.borrow(1e18, griefer);
33+
skip(1);
34+
35+
// Do a swap
36+
37+
uint256 amountIn = 1e18;
38+
uint256 amountOut =
39+
periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), amountIn);
40+
assertApproxEqAbs(amountOut, 0.9974e18, 0.0001e18);
41+
42+
// Honest deposit
43+
assetTST.mint(address(this), amountIn);
44+
assetTST.transfer(address(eulerSwap), amountIn);
45+
46+
// Griefer front-runs with 1 wei deposit, which rounds down to 0 shares
47+
assetTST2.mint(address(this), 1);
48+
assetTST2.transfer(address(eulerSwap), 1);
49+
50+
// Naive deposit() would fail with E_ZeroShares
51+
eulerSwap.swap(0, amountOut, address(this), "");
52+
53+
assertEq(assetTST2.balanceOf(address(this)), amountOut);
54+
55+
assertEq(assetTST2.balanceOf(address(eulerSwap)), 1); // griefing transfer was untouched
56+
}
57+
58+
function test_depositFailure() public monotonicHolderNAV {
59+
// Do a swap
60+
61+
uint256 amountIn = 1e18;
62+
uint256 amountOut =
63+
periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), amountIn);
64+
assertApproxEqAbs(amountOut, 0.9974e18, 0.0001e18);
65+
66+
// Honest deposit
67+
assetTST.mint(address(this), amountIn);
68+
assetTST.transfer(address(eulerSwap), amountIn);
69+
70+
// Griefer front-runs with 1 wei deposit, which rounds down to 0 shares
71+
assetTST2.mint(address(this), 1);
72+
assetTST2.transfer(address(eulerSwap), 1);
73+
74+
// Force deposits to fail
75+
eTST2.setHookConfig(address(0), EVKConstants.OP_DEPOSIT);
76+
77+
vm.expectRevert(
78+
abi.encodeWithSelector(
79+
EulerSwap.DepositFailure.selector, abi.encodeWithSelector(EVKErrors.E_OperationDisabled.selector)
80+
)
81+
);
82+
eulerSwap.swap(0, amountOut, address(this), "");
83+
84+
assertEq(assetTST2.balanceOf(address(this)), 0);
85+
86+
assertEq(assetTST2.balanceOf(address(eulerSwap)), 1); // griefing transfer was untouched
87+
}
88+
}

0 commit comments

Comments
 (0)