Skip to content

Commit 5048fc6

Browse files
committed
quotePadding, add new test
1 parent b818ed3 commit 5048fc6

File tree

4 files changed

+151
-10
lines changed

4 files changed

+151
-10
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ With careful parameter selection, the EulerSwap curve supports optimal tradeoffs
118118

119119
* The EulerSwap curve has some numerical instability that we believe is caused by rounding
120120
* We think the biggest effect is that it may cause some swaps to fail even if the exact quoted amount is sent. Also, it may result in users slightly overpaying for swaps.
121-
* The code currently has a roundingCompensation adjustment that seems to prevent this, but since we don't know a hard upper-bound it's hard to say if this solves it in all cases
121+
* The code currently has a quotePadding adjustment that seems to prevent this, but since we don't know a hard upper-bound it's hard to say if this solves it in all cases. In particular, this will only work well for 18-decimal tokens of reasonable prices
122122
* Currently we have only been supporting stable-stable pairs
123123
* What extra considerations would there be for floating pairs?
124124
* Automatically re-invest fees. There are a few options:

src/MaglevEulerSwap.sol

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,6 @@ contract MaglevEulerSwap is IMaglevEulerSwap, MaglevBase {
3434
initialReserve1 = reserve1;
3535
}
3636

37-
// Due to rounding, computeQuote() may underestimate the amount required to
38-
// pass the verify() function. In order to prevent swaps from failing, quotes
39-
// are inflated by this compensation factor. FIXME: solve the rounding.
40-
uint256 private constant roundingCompensation = 1.0000000000001e18;
41-
4237
function verify(uint256 newReserve0, uint256 newReserve1) internal view virtual override {
4338
int256 delta = 0;
4439

@@ -54,6 +49,11 @@ contract MaglevEulerSwap is IMaglevEulerSwap, MaglevBase {
5449
require(delta >= 0, KNotSatisfied());
5550
}
5651

52+
// Due to rounding, computeQuote() may underestimate the amount required to
53+
// pass the verify() function. In order to prevent swaps from failing, quotes
54+
// are inflated by this padding factor.
55+
uint256 private constant quotePadding = 1.00000000001e18;
56+
5757
function computeQuote(uint256 amount, bool exactIn, bool asset0IsInput)
5858
internal
5959
view
@@ -112,11 +112,11 @@ contract MaglevEulerSwap is IMaglevEulerSwap, MaglevBase {
112112
if (exactIn) {
113113
if (asset0IsInput) output = uint256(-dy);
114114
else output = uint256(-dx);
115-
output = output * 1e18 / roundingCompensation;
115+
output = output * 1e18 / quotePadding;
116116
} else {
117117
if (asset0IsInput) output = uint256(dx);
118118
else output = uint256(dy);
119-
output = output * roundingCompensation / 1e18;
119+
output = output * quotePadding / 1e18;
120120
}
121121
}
122122

test/EulerSwap.t.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ contract EulerSwapTest is MaglevTestBase {
173173
assertGe(getHolderNAV(), origNAV);
174174
}
175175

176-
// To reproduce, change roundingCompensation to 1e18
176+
// To reproduce, change quotePadding to 1e18
177177
function test_roundingFailure() public {
178178
uint256 amountIn = 1.4e18;
179179
uint256 amountOut = maglev.quoteExactInput(address(assetTST), address(assetTST2), amountIn);
@@ -210,7 +210,7 @@ contract EulerSwapTest is MaglevTestBase {
210210
t1.transfer(address(maglev), amount);
211211

212212
{
213-
uint256 qPlus = q * 1.0000000000002e18 / 1e18;
213+
uint256 qPlus = q * 1.00000000002e18 / 1e18;
214214
vm.expectRevert();
215215
if (dir) maglev.swap(0, qPlus, address(this), "");
216216
else maglev.swap(qPlus, 0, address(this), "");

test/PreserveNav.t.sol

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
3+
pragma solidity ^0.8.24;
4+
5+
import {Test, console} from "forge-std/Test.sol";
6+
7+
import {TestERC20} from "evk-test/unit/evault/EVaultTestBase.t.sol";
8+
import {IEVault} from "evk/EVault/IEVault.sol";
9+
import {MaglevTestBase} from "./MaglevTestBase.t.sol";
10+
11+
import {MaglevEulerSwap as Maglev, MaglevBase} from "../src/MaglevEulerSwap.sol";
12+
13+
contract PreserveNav is MaglevTestBase {
14+
Maglev public maglev;
15+
16+
function setUp() public virtual override {
17+
super.setUp();
18+
}
19+
20+
function createMaglev(
21+
uint112 debtLimitA,
22+
uint112 debtLimitB,
23+
uint256 fee,
24+
uint256 px,
25+
uint256 py,
26+
uint256 cx,
27+
uint256 cy
28+
) internal {
29+
vm.prank(creator);
30+
maglev = new Maglev(
31+
getMaglevBaseParams(debtLimitA, debtLimitB, fee),
32+
Maglev.EulerSwapParams({priceX: px, priceY: py, concentrationX: cx, concentrationY: cy})
33+
);
34+
35+
vm.prank(holder);
36+
evc.setAccountOperator(holder, address(maglev), true);
37+
38+
vm.prank(anyone);
39+
maglev.configure();
40+
}
41+
42+
43+
44+
function test_preserve_nav(uint256 cx, uint256 cy, uint256 fee, bool preSkimDir, bool dir1, uint256 amount1, bool skimOrder1, bool dir2, uint256 amount2, bool skimOrder2) public {
45+
cx = bound(cx, 0.1e18, 0.99e18);
46+
cy = bound(cy, 0.1e18, 0.99e18);
47+
fee = bound(fee, 0, 0.2e18);
48+
amount1 = bound(amount1, 0.00001e18, 25e18);
49+
amount2 = bound(amount2, 0.00001e18, 25e18);
50+
51+
if (fee < 0.1e18) fee = 0; // half of the time use fee 0
52+
else fee -= 0.1e18;
53+
54+
createMaglev(50e18, 50e18, fee, 1e18, 1e18, cx, cy);
55+
56+
skimAll(preSkimDir);
57+
int256 nav1 = getHolderNAV();
58+
59+
{
60+
TestERC20 t1;
61+
TestERC20 t2;
62+
if (dir1) (t1, t2) = (assetTST, assetTST2);
63+
else (t1, t2) = (assetTST2, assetTST);
64+
65+
uint256 q = maglev.quoteExactInput(address(t1), address(t2), amount1);
66+
67+
t1.mint(address(this), amount1);
68+
t1.transfer(address(maglev), amount1);
69+
if (dir1) maglev.swap(0, q, address(this), "");
70+
else maglev.swap(q, 0, address(this), "");
71+
72+
skimAll(skimOrder1);
73+
}
74+
75+
assertGe(getHolderNAV(), nav1);
76+
77+
{
78+
TestERC20 t1;
79+
TestERC20 t2;
80+
if (dir2) (t1, t2) = (assetTST, assetTST2);
81+
else (t1, t2) = (assetTST2, assetTST);
82+
83+
uint256 q = maglev.quoteExactInput(address(t1), address(t2), amount2);
84+
85+
t1.mint(address(this), amount2);
86+
t1.transfer(address(maglev), amount2);
87+
if (dir2) maglev.swap(0, q, address(this), "");
88+
else maglev.swap(q, 0, address(this), "");
89+
90+
skimAll(skimOrder2);
91+
}
92+
93+
assertGe(getHolderNAV(), nav1);
94+
}
95+
96+
97+
98+
function _skimAll(bool dir) public returns (uint256) {
99+
uint256 skimmed = 0;
100+
uint256 val = 1;
101+
102+
// Phase 1: Keep doubling skim amount until it fails
103+
104+
while (true) {
105+
(uint256 amount0, uint256 amount1) = dir ? (val, uint256(0)) : (uint256(0), val);
106+
107+
try maglev.swap(amount0, amount1, address(0xDEAD), "") {
108+
skimmed += val;
109+
val *= 2;
110+
} catch {
111+
break;
112+
}
113+
}
114+
115+
// Phase 2: Keep halving skim amount until 1 wei skim fails
116+
117+
while (true) {
118+
if (val > 1) val /= 2;
119+
120+
(uint256 amount0, uint256 amount1) = dir ? (val, uint256(0)) : (uint256(0), val);
121+
122+
try maglev.swap(amount0, amount1, address(0xDEAD), "") {
123+
skimmed += val;
124+
} catch {
125+
if (val == 1) break;
126+
}
127+
}
128+
129+
return skimmed;
130+
}
131+
132+
function skimAll(bool order) public {
133+
if (order) {
134+
_skimAll(true);
135+
_skimAll(false);
136+
} else {
137+
_skimAll(false);
138+
_skimAll(true);
139+
}
140+
}
141+
}

0 commit comments

Comments
 (0)