Skip to content

Commit 6292995

Browse files
authored
Merge pull request #31 from euler-xyz/csec-fixes
Csec fixes
2 parents 873c045 + 3ea2b45 commit 6292995

File tree

5 files changed

+189
-15
lines changed

5 files changed

+189
-15
lines changed

docs/f.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
## Implementation of `f`
2+
3+
`f` (aka the "EulerSwap Function") is a parameterisable curve that defines the boundary of permissible points for EulerSwap AMMs. Points on the curve or above and to-the right are allowed, others are not.
4+
5+
Only formula 3 from the whitepaper is implemented in the EulerSwap core, since this can be used for both domains of the curve by mirroring the parameters. The more complicated formula 4 is a closed-form method for quoting swaps so it can be implemented in a periphery (if desired).
6+
7+
### Derivation
8+
9+
Formula 3 from the whitepaper:
10+
11+
y0 + (px / py) * (x0 - x) * (c + (1 - c) * (x0 / x))
12+
13+
Multiply second term by `x/x`:
14+
15+
y0 + (px / py) * (x0 - x) * ((c * x) + (1 - c) * x0) / x
16+
17+
`c` is scaled by `1e18`:
18+
19+
y0 + (px / py) * (x0 - x) * ((c * x) + (1e18 - c) * x0) / (x * 1e18)
20+
21+
Re-order division by `py`:
22+
23+
y0 + px * (x0 - x) * ((c * x) + (1e18 - c) * x0) / (x * 1e18) / py
24+
25+
Use `mulDiv` to avoid intermediate overflow:
26+
27+
y0 + Math.mulDiv(px * (x0 - x), c * x + (1e18 - c) * x0, x * 1e18) / py
28+
29+
Round up for both divisions (operation is distributive):
30+
31+
y0 + (Math.mulDiv(px * (x0 - x), c * x + (1e18 - c) * x0, x * 1e18, Math.Rounding.Ceil) + (py-1)) / py
32+
33+
### Boundary Analysis
34+
35+
Pre-conditions: x <= x0, 1 <= {px,py} <= 1e36, {x0,y0} <= type(uint112).max, c <= 1e18
36+
37+
None of the computations for the arguments to `mulDiv` can overflow:
38+
39+
* Arg 1: `px * (x0 - x)`
40+
* Upper-bound: `1e36*(2**112 - 1) =~ 232 bits`
41+
* Arg 2: `c * x + (1e18 - c) * x0`
42+
* Upper-bound: `1e18*(2**112 - 1)*2 =~ 173 bits`
43+
* Arg 3: `x * 1e18`
44+
* Upper-bound: `1e18*(2**112 - 1) =~ 172 bits`
45+
46+
If amounts/prices are large, and we travel too far down the curve, then `mulDiv` (or the subsequent `y0` addition) could overflow because its output value cannot be represented as a `uint256`. However, these output values would never be valid anyway, because they exceed `type(uint112).max`.
47+
48+
To see this, consider the case where `mulDiv` fails due to overflow. This means that its result would've been greater than `2**256 - 1`. Dividing this value by the largest allowed value for `py` (`1e36`) gives approximately `2**136`, which is greater than the maximum allowed amount value of `2**112 - 1`. Both the rounding up operation and the final addition of `y0` can only further *increase* this value. This means that all cases where `mulDiv` or the subsequent additions overflow would involve `f()` returning values that are impossible for a swapper to satisfy, so they would revert anyways.
49+
50+
### Unchecked Math
51+
52+
As-per the previous section, none of the computations of the arguments to `mulDiv` can overflow. To prevent overflows in the remaining operations, the `mulDiv` output is further restricted to `2**248 - 1`:
53+
54+
unchecked {
55+
uint256 v = Math.mulDiv(px * (x0 - x), c * x + (1e18 - c) * x0, x * 1e18, Math.Rounding.Ceil);
56+
require(v <= type(uint248).max, Overflow());
57+
return y0 + (v + (py - 1)) / py;
58+
}
59+
60+
Note that this does not change the analysis of the previous section: Values between `2**248 - 1` and `2**256 - 1` will also never reduce down to the required `2**112 - 1`, so this does not cause any additional failure cases.

src/EulerSwap.sol

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ pragma solidity ^0.8.27;
44
import {SafeERC20, IERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol";
55
import {IEVC} from "evc/interfaces/IEthereumVaultConnector.sol";
66
import {IEVault, IBorrowing, IERC4626, IRiskManager} from "evk/EVault/IEVault.sol";
7+
import {Errors as EVKErrors} from "evk/EVault/shared/Errors.sol";
78
import {IUniswapV2Callee} from "./interfaces/IUniswapV2Callee.sol";
89
import {IEulerSwap} from "./interfaces/IEulerSwap.sol";
910
import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol";
1011
import {EVCUtil} from "evc/utils/EVCUtil.sol";
12+
import {Math} from "openzeppelin-contracts/utils/math/Math.sol";
1113

1214
contract EulerSwap is IEulerSwap, EVCUtil {
1315
using SafeERC20 for IERC20;
@@ -48,10 +50,11 @@ contract EulerSwap is IEulerSwap, EVCUtil {
4850

4951
error Locked();
5052
error Overflow();
51-
error BadFee();
53+
error BadParam();
5254
error DifferentEVC();
5355
error AssetsOutOfOrderOrEqual();
5456
error CurveViolation();
57+
error DepositFailure(bytes reason);
5558

5659
modifier nonReentrant() {
5760
if (status == 0) activate();
@@ -64,7 +67,11 @@ contract EulerSwap is IEulerSwap, EVCUtil {
6467
constructor(Params memory params, CurveParams memory curveParams) EVCUtil(IEVault(params.vault0).EVC()) {
6568
// EulerSwap params
6669

67-
require(params.fee < 1e18, BadFee());
70+
require(params.fee < 1e18, BadParam());
71+
require(params.debtLimit0 <= type(uint112).max && params.debtLimit1 <= type(uint112).max, BadParam());
72+
require(curveParams.priceX > 0 && curveParams.priceY > 0, BadParam());
73+
require(curveParams.priceX <= 1e36 && curveParams.priceY <= 1e36, BadParam());
74+
require(curveParams.concentrationX <= 1e18 && curveParams.concentrationY <= 1e18, BadParam());
6875
require(IEVault(params.vault0).EVC() == IEVault(params.vault1).EVC(), DifferentEVC());
6976

7077
address asset0Addr = IEVault(params.vault0).asset();
@@ -110,16 +117,10 @@ contract EulerSwap is IEulerSwap, EVCUtil {
110117
// Deposit all available funds, adjust received amounts downward to collect fees
111118

112119
uint256 amount0In = IERC20(asset0).balanceOf(address(this));
113-
if (amount0In > 0) {
114-
depositAssets(vault0, amount0In);
115-
amount0In = amount0In * feeMultiplier / 1e18;
116-
}
120+
if (amount0In > 0) amount0In = depositAssets(vault0, amount0In) * feeMultiplier / 1e18;
117121

118122
uint256 amount1In = IERC20(asset1).balanceOf(address(this));
119-
if (amount1In > 0) {
120-
depositAssets(vault1, amount1In);
121-
amount1In = amount1In * feeMultiplier / 1e18;
122-
}
123+
if (amount1In > 0) amount1In = depositAssets(vault1, amount1In) * feeMultiplier / 1e18;
123124

124125
// Verify curve invariant is satisified
125126

@@ -201,8 +202,12 @@ contract EulerSwap is IEulerSwap, EVCUtil {
201202
}
202203
}
203204

204-
function depositAssets(address vault, uint256 amount) internal {
205-
IEVault(vault).deposit(amount, myAccount);
205+
function depositAssets(address vault, uint256 amount) internal returns (uint256) {
206+
try IEVault(vault).deposit(amount, myAccount) {}
207+
catch (bytes memory reason) {
208+
require(bytes4(reason) == EVKErrors.E_ZeroShares.selector, DepositFailure(reason));
209+
return 0;
210+
}
206211

207212
if (IEVC(evc).isControllerEnabled(myAccount, vault)) {
208213
IEVC(evc).call(
@@ -213,6 +218,8 @@ contract EulerSwap is IEulerSwap, EVCUtil {
213218
IEVC(evc).call(vault, myAccount, 0, abi.encodeCall(IRiskManager.disableController, ()));
214219
}
215220
}
221+
222+
return amount;
216223
}
217224

218225
function myDebt(address vault) internal view returns (uint256) {
@@ -239,7 +246,12 @@ contract EulerSwap is IEulerSwap, EVCUtil {
239246
}
240247

241248
/// @dev EulerSwap curve definition
242-
function f(uint256 xt, uint256 px, uint256 py, uint256 x0, uint256 y0, uint256 c) internal pure returns (uint256) {
243-
return y0 + px * 1e18 / py * (c * (2 * x0 - xt) / 1e18 + (1e18 - c) * x0 / 1e18 * x0 / xt - x0) / 1e18;
249+
/// Pre-conditions: x <= x0, 1 <= {px,py} <= 1e36, {x0,y0} <= type(uint112).max, c <= 1e18
250+
function f(uint256 x, uint256 px, uint256 py, uint256 x0, uint256 y0, uint256 c) internal pure returns (uint256) {
251+
unchecked {
252+
uint256 v = Math.mulDiv(px * (x0 - x), c * x + (1e18 - c) * x0, x * 1e18, Math.Rounding.Ceil);
253+
require(v <= type(uint248).max, Overflow());
254+
return y0 + (v + (py - 1)) / py;
255+
}
244256
}
245257
}

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+
}

test/EulerSwapFactoryTest.t.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ contract EulerSwapFactoryTest is EulerSwapTestBase {
7979

8080
function testDeployWithBadFee() public {
8181
vm.prank(creator);
82-
vm.expectRevert(EulerSwap.BadFee.selector);
82+
vm.expectRevert(EulerSwap.BadParam.selector);
8383
eulerSwapFactory.deployPool(
8484
IEulerSwapFactory.DeployParams(
8585
address(eTST), address(eTST2), holder, 1e18, 1e18, 1e18, 0.4e18, 0.85e18, 50e18, 50e18

test/EulerSwapTest.t.sol

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,4 +175,18 @@ contract EulerSwapTest is EulerSwapTestBase {
175175
assertGe(getHolderNAV(), origNAV);
176176
}
177177
}
178+
179+
/*
180+
// Make `f()` function public to run this test
181+
function test_fFuncOverflow(uint256 xt, uint256 px, uint256 py, uint256 x0, uint256 y0, uint256 c) public view {
182+
x0 = bound(x0, 1, type(uint112).max);
183+
y0 = bound(y0, 0, type(uint112).max);
184+
xt = bound(xt, 1 + x0 / 1e3, x0); // thousand-fold price movement
185+
px = bound(px, 1, 1e36);
186+
py = bound(py, 1, 1e36);
187+
c = bound(c, 1, 1e18);
188+
189+
eulerSwap.f(xt, px, py, x0, y0, c);
190+
}
191+
*/
178192
}

0 commit comments

Comments
 (0)