Skip to content

Commit 969078b

Browse files
committed
convergent fee curve
1 parent c906f4b commit 969078b

File tree

6 files changed

+202
-212
lines changed

6 files changed

+202
-212
lines changed

spot-vaults/contracts/BillBroker.sol

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ contract BillBroker is
7272

7373
// Math
7474
using MathUpgradeable for uint256;
75+
using MathHelpers for uint256;
7576
using MathHelpers for int256;
7677
using LineHelpers for Line;
7778

@@ -193,14 +194,8 @@ contract BillBroker is
193194
BillBrokerFees({
194195
mintFeePerc: 0,
195196
burnFeePerc: 0,
196-
perpToUSDSwapFeeFactors: Range({
197-
lower: MAX_FEE_FACTOR,
198-
upper: MAX_FEE_FACTOR
199-
}),
200-
usdToPerpSwapFeeFactors: Range({
201-
lower: MAX_FEE_FACTOR,
202-
upper: MAX_FEE_FACTOR
203-
}),
197+
perpToUSDSwapFeeFactors: Range({ lower: ONE, upper: ONE }),
198+
usdToPerpSwapFeeFactors: Range({ lower: ONE, upper: ONE }),
204199
protocolSwapSharePerc: 0
205200
})
206201
);
@@ -237,11 +232,26 @@ contract BillBroker is
237232
if (
238233
fees_.mintFeePerc > ONE ||
239234
fees_.burnFeePerc > ONE ||
235+
fees_.protocolSwapSharePerc > ONE
236+
) {
237+
revert InvalidPerc();
238+
}
239+
240+
// NOTE: The implementation limits the max -ve fee paid when swapping to MIN_FEE_FACTOR (or -20%).
241+
// The -ve fee limit for a given swap direction inferred from `fee.upper` from the opposite direction.
242+
// We thus also limit `fee.upper` to 1.2 (or +20%).
243+
if (
240244
fees_.perpToUSDSwapFeeFactors.lower > fees_.perpToUSDSwapFeeFactors.upper ||
241245
fees_.perpToUSDSwapFeeFactors.lower < ONE ||
246+
fees_.perpToUSDSwapFeeFactors.upper > (TWO - MIN_FEE_FACTOR)
247+
) {
248+
revert InvalidPerc();
249+
}
250+
251+
if (
242252
fees_.usdToPerpSwapFeeFactors.lower > fees_.usdToPerpSwapFeeFactors.upper ||
243253
fees_.usdToPerpSwapFeeFactors.lower < ONE ||
244-
fees_.protocolSwapSharePerc > ONE
254+
fees_.usdToPerpSwapFeeFactors.upper > (TWO - MIN_FEE_FACTOR)
245255
) {
246256
revert InvalidPerc();
247257
}
@@ -836,8 +846,8 @@ contract BillBroker is
836846
}
837847

838848
Range memory feeP2U = fees.perpToUSDSwapFeeFactors;
839-
Range memory feeU2P = fees.usdToPerpSwapFeeFactors;
840-
uint256 feeU2PDelta = feeU2P.upper - feeU2P.lower;
849+
// The maximum -ve fee, inferred from fees for the other swap direction.
850+
uint256 feeU2PUpperInv = (TWO - fees.usdToPerpSwapFeeFactors.upper);
841851
return
842852
LineHelpers
843853
.computePiecewiseAvgY(
@@ -857,12 +867,12 @@ contract BillBroker is
857867
x1: arSoftBound.upper,
858868
y1: feeP2U.lower,
859869
x2: arHardBound.upper,
860-
y2: (feeP2U.lower - feeU2PDelta)
870+
y2: feeU2PUpperInv
861871
}),
862872
arSoftBound,
863873
Range({ lower: arPost, upper: arPre })
864874
)
865-
.clip(MIN_FEE_FACTOR, MAX_FEE_FACTOR);
875+
.clip(feeU2PUpperInv, feeP2U.upper);
866876
}
867877

868878
/// @notice Computes the swap fee factor when swapping from usd to perp tokens.
@@ -881,14 +891,14 @@ contract BillBroker is
881891
}
882892

883893
Range memory feeU2P = fees.usdToPerpSwapFeeFactors;
884-
Range memory feeP2U = fees.perpToUSDSwapFeeFactors;
885-
uint256 feeP2UDelta = feeP2U.upper - feeP2U.lower;
894+
// The maximum -ve fee, inferred from fees for the other swap direction.
895+
uint256 feeP2UUpperInv = (TWO - fees.perpToUSDSwapFeeFactors.upper);
886896
return
887897
LineHelpers
888898
.computePiecewiseAvgY(
889899
Line({
890900
x1: arHardBound.lower,
891-
y1: (feeU2P.lower - feeP2UDelta),
901+
y1: feeP2UUpperInv,
892902
x2: arSoftBound.lower,
893903
y2: feeU2P.lower
894904
}),
@@ -907,7 +917,7 @@ contract BillBroker is
907917
arSoftBound,
908918
Range({ lower: arPre, upper: arPost })
909919
)
910-
.clip(MIN_FEE_FACTOR, MAX_FEE_FACTOR);
920+
.clip(feeP2UUpperInv, feeU2P.upper);
911921
}
912922

913923
/// @param s The system reserve state.

spot-vaults/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
"ethers": "^6.6.0",
6363
"ethers-v5": "npm:ethers@^5.7.0",
6464
"ganache-cli": "latest",
65-
"hardhat": "^2.22.17",
65+
"hardhat": "^2.22.18",
6666
"hardhat-gas-reporter": "latest",
6767
"lodash": "^4.17.21",
6868
"prettier": "^2.7.1",

spot-vaults/test/BillBroker.ts

Lines changed: 44 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,10 @@ describe("BillBroker", function () {
5252
const fees = await billBroker.fees();
5353
expect(fees.mintFeePerc).to.eq(0);
5454
expect(fees.burnFeePerc).to.eq(0);
55-
expect(fees.perpToUSDSwapFeeFactors.lower).to.eq(percFP("2"));
56-
expect(fees.perpToUSDSwapFeeFactors.upper).to.eq(percFP("2"));
57-
expect(fees.usdToPerpSwapFeeFactors.lower).to.eq(percFP("2"));
58-
expect(fees.usdToPerpSwapFeeFactors.upper).to.eq(percFP("2"));
55+
expect(fees.perpToUSDSwapFeeFactors.lower).to.eq(percFP("1"));
56+
expect(fees.perpToUSDSwapFeeFactors.upper).to.eq(percFP("1"));
57+
expect(fees.usdToPerpSwapFeeFactors.lower).to.eq(percFP("1"));
58+
expect(fees.usdToPerpSwapFeeFactors.upper).to.eq(percFP("1"));
5959
expect(fees.protocolSwapSharePerc).to.eq(0);
6060

6161
expect(await billBroker.usdBalance()).to.eq(0n);
@@ -452,16 +452,16 @@ describe("BillBroker", function () {
452452

453453
expect(
454454
await billBroker.computeUSDToPerpSwapFeeFactor(percFP("0.1"), percFP("0.26")),
455-
).to.eq(percFP("0.8711"));
455+
).to.eq(percFP("0.84"));
456456
expect(
457457
await billBroker.computeUSDToPerpSwapFeeFactor(percFP("0.3"), percFP("0.5")),
458-
).to.eq(percFP("0.9305"));
458+
).to.eq(percFP("0.8955"));
459459
expect(
460460
await billBroker.computeUSDToPerpSwapFeeFactor(percFP("0.25"), percFP("1")),
461-
).to.eq(percFP("0.98"));
461+
).to.eq(percFP("0.963333333333333333"));
462462
expect(
463463
await billBroker.computeUSDToPerpSwapFeeFactor(percFP("0.25"), percFP("1.24")),
464-
).to.eq(percFP("0.990909090909090909"));
464+
).to.eq(percFP("0.978282828282828282"));
465465
expect(
466466
await billBroker.computeUSDToPerpSwapFeeFactor(percFP("0.76"), percFP("1.24")),
467467
).to.eq(percFP("1.025"));
@@ -488,32 +488,6 @@ describe("BillBroker", function () {
488488
).to.eq(percFP("2"));
489489
});
490490

491-
it("should compute the right fee factor when outside bounds", async function () {
492-
const { billBroker } = await loadFixture(setupContracts);
493-
await billBroker.updateARBounds(
494-
[percFP("0.75"), percFP("1.25")],
495-
[percFP("0"), percFP("10")],
496-
);
497-
498-
await billBroker.updateFees({
499-
mintFeePerc: 0n,
500-
burnFeePerc: 0n,
501-
perpToUSDSwapFeeFactors: {
502-
lower: percFP("1"),
503-
upper: percFP("1"),
504-
},
505-
usdToPerpSwapFeeFactors: {
506-
lower: percFP("2.01"),
507-
upper: percFP("3"),
508-
},
509-
protocolSwapSharePerc: 0n,
510-
});
511-
512-
expect(
513-
await billBroker.computeUSDToPerpSwapFeeFactor(percFP("1"), percFP("1.25")),
514-
).to.eq(percFP("2"));
515-
});
516-
517491
describe("Extended coverage for break-point conditions & edge cases", function () {
518492
let billBroker;
519493

@@ -546,15 +520,15 @@ describe("BillBroker", function () {
546520
percFP("0.50"),
547521
percFP("0.55"),
548522
);
549-
expect(result).to.eq(percFP("1.001645"));
523+
expect(result).to.eq(percFP("0.979145"));
550524
});
551525

552526
it("Case B: Range straddles arSoftBound.lower => partial weighting fn1/fn2", async () => {
553527
const result = await billBroker.computeUSDToPerpSwapFeeFactor(
554528
percFP("0.70"),
555529
percFP("0.80"),
556530
);
557-
expect(result).to.eq(percFP("1.0237025"));
531+
expect(result).to.eq(percFP("1.0224525"));
558532
});
559533

560534
it("Case C: Range fully within [arSoftBound.lower..arSoftBound.upper] => uses fn2 entirely", async () => {
@@ -596,19 +570,19 @@ describe("BillBroker", function () {
596570
const { billBroker } = await loadFixture(setupContracts);
597571
await billBroker.updateARBounds(
598572
[percFP("0.75"), percFP("1.25")],
599-
[percFP("0.5"), percFP("1.5")],
573+
[percFP("0.25"), percFP("4")],
600574
);
601575

602576
await billBroker.updateFees({
603577
mintFeePerc: 0n,
604578
burnFeePerc: 0n,
605579
perpToUSDSwapFeeFactors: {
606580
lower: percFP("1.1"),
607-
upper: percFP("1.5"),
581+
upper: percFP("1.15"),
608582
},
609583
usdToPerpSwapFeeFactors: {
610-
lower: percFP("1"),
611-
upper: percFP("1"),
584+
lower: percFP("1.025"),
585+
upper: percFP("1.2"),
612586
},
613587
protocolSwapSharePerc: 0n,
614588
});
@@ -620,47 +594,45 @@ describe("BillBroker", function () {
620594
billBroker.computePerpToUSDSwapFeeFactor(percFP("1.5"), percFP("0.5")),
621595
).to.be.revertedWithCustomError(billBroker, "UnexpectedRangeDelta");
622596

597+
expect(
598+
await billBroker.computePerpToUSDSwapFeeFactor(percFP("4"), percFP("3")),
599+
).to.eq(percFP("0.854545454545454545"));
600+
601+
expect(
602+
await billBroker.computePerpToUSDSwapFeeFactor(percFP("4"), percFP("1.25")),
603+
).to.eq(percFP("0.95"));
604+
605+
expect(
606+
await billBroker.computePerpToUSDSwapFeeFactor(percFP("3"), percFP("2")),
607+
).to.eq(percFP("0.963636363636363636"));
608+
609+
expect(
610+
await billBroker.computePerpToUSDSwapFeeFactor(percFP("2"), percFP("1.5")),
611+
).to.eq(percFP("1.045454545454545454"));
612+
623613
expect(
624614
await billBroker.computePerpToUSDSwapFeeFactor(percFP("2"), percFP("0.8")),
625-
).to.eq(percFP("1.1"));
615+
).to.eq(percFP("1.074431818181818181"));
616+
626617
expect(
627618
await billBroker.computePerpToUSDSwapFeeFactor(percFP("1.45"), percFP("0.8")),
619+
).to.eq(percFP("1.096643356643356643"));
620+
621+
expect(
622+
await billBroker.computePerpToUSDSwapFeeFactor(percFP("1.25"), percFP("0.9")),
628623
).to.eq(percFP("1.1"));
624+
629625
expect(
630626
await billBroker.computePerpToUSDSwapFeeFactor(percFP("0.8"), percFP("0.7")),
631-
).to.eq(percFP("1.12"));
627+
).to.eq(percFP("1.10125"));
628+
632629
expect(
633630
await billBroker.computePerpToUSDSwapFeeFactor(percFP("0.8"), percFP("0.5")),
634-
).to.eq(percFP("1.266666666666666666"));
635-
expect(
636-
await billBroker.computePerpToUSDSwapFeeFactor(percFP("1.0"), percFP("0.49")),
637-
).to.eq(percFP("2"));
638-
});
639-
640-
it("should compute the right fee factor when outside bounds", async function () {
641-
const { billBroker } = await loadFixture(setupContracts);
642-
await billBroker.updateARBounds(
643-
[percFP("0.75"), percFP("1.25")],
644-
[percFP("0"), percFP("10")],
645-
);
646-
647-
await billBroker.updateFees({
648-
mintFeePerc: 0n,
649-
burnFeePerc: 0n,
650-
perpToUSDSwapFeeFactors: {
651-
lower: percFP("2.01"),
652-
upper: percFP("3"),
653-
},
654-
usdToPerpSwapFeeFactors: {
655-
lower: percFP("1"),
656-
upper: percFP("1"),
657-
},
658-
protocolSwapSharePerc: 0n,
659-
});
631+
).to.eq(percFP("1.110416666666666666"));
660632

661633
expect(
662-
await billBroker.computePerpToUSDSwapFeeFactor(percFP("1.25"), percFP("1.11")),
663-
).to.eq(percFP("2"));
634+
await billBroker.computePerpToUSDSwapFeeFactor(percFP("1.0"), percFP("0.49")),
635+
).to.eq(percFP("1.106627450980392156"));
664636
});
665637

666638
describe("Extended coverage for break-point conditions & edge cases", function () {
@@ -719,15 +691,15 @@ describe("BillBroker", function () {
719691
percFP("1.30"),
720692
percFP("1.20"),
721693
);
722-
expect(result).to.eq(percFP("1.024344090909090909"));
694+
expect(result).to.eq(percFP("1.024116818181818182"));
723695
});
724696

725697
it("Case E: Entirely above arSoftBound.upper => uses fn3", async () => {
726698
const result = await billBroker.computePerpToUSDSwapFeeFactor(
727699
percFP("2.50"),
728700
percFP("2.0"),
729701
);
730-
expect(result).to.eq(percFP("0.972527272727272727"));
702+
expect(result).to.eq(percFP("0.954345454545454546"));
731703
});
732704

733705
it("Zero-length range exactly at boundary => picks boundary side", async () => {

0 commit comments

Comments
 (0)