Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/perps-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add optional `feeSource` to `FeeCalculationResult`, reporting which fee source (`default`, `rewards` or `subscription`) won the fee resolution the quoted MetaMask fee was priced from, so clients can attribute the discount shown in a fee preview without re-deriving it from the rates ([#10650](https://github.com/MetaMask/core/pull/10650))
- Absent when the quote was not re-priced from a fee resolution, for example a placement that carries no MetaMask builder fee
- `rewards` can carry a 0% discount, so check `metamaskFeeRate` before presenting it as a reduction
- Add optional `metamaskFeeDiscountBips` and `undiscountedMetamaskFeeRate` to `FeeCalculationResult`, so clients can show the original MetaMask fee next to the discounted one without the default builder fee constant ([#10683](https://github.com/MetaMask/core/pull/10683))
- `metamaskFeeDiscountBips` is the discount the winning fee source applied, in basis points off the default builder fee; `undiscountedMetamaskFeeRate` is the MetaMask fee rate before it
- `metamaskFeeDiscountBips` is `0` when no reduction applied: when `default` won, or when `rewards` won with a 0% discount. A `feeSource` other than `default` does not imply a discount
- `metamaskFeeRate` is already discounted; do not apply `metamaskFeeDiscountBips` to it again
- Absent whenever `feeSource` is absent, for example a placement that carries no MetaMask builder fee

### Changed

Expand Down
29 changes: 28 additions & 1 deletion packages/perps-controller/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1793,7 +1793,7 @@ export type FeeCalculationResult = {
protocolFeeAmount?: number; // Protocol fee amount in USD

// MetaMask builder/revenue fee
metamaskFeeRate?: number; // MetaMask fee rate (e.g., 0.001 for 0.1%), undefined when unavailable
metamaskFeeRate?: number; // MetaMask fee rate (e.g., 0.001 for 0.1%), undefined when unavailable. Already discounted: do not apply `metamaskFeeDiscountBips` to it again
metamaskFeeAmount?: number; // MetaMask fee amount in USD

/**
Expand Down Expand Up @@ -1822,6 +1822,33 @@ export type FeeCalculationResult = {
*/
feeSource?: PerpsFeeSource;

/**
* Discount the `feeSource` winner applied, in basis points off the default
* MetaMask builder fee (e.g. 6500 for 65% off). `0` when no reduction
* applied: when `default` won, or when `rewards` won with a 0% discount. A
* `feeSource` other than `default` therefore does not imply a discount; check
* this value instead.
*
* Already applied: `metamaskFeeRate` is `undiscountedMetamaskFeeRate`
* reduced by this discount and floored to the tenth of a basis point the
* venue charges. Do not discount `metamaskFeeRate` again. The floor can make
* the actual reduction slightly larger than this figure, so derive savings
* amounts from the two rates.
*
* Absent under the same rule as `feeSource`.
*/
metamaskFeeDiscountBips?: number;

/**
* MetaMask fee rate before `metamaskFeeDiscountBips` was applied, as a
* decimal (e.g. 0.001 for 0.1%). Equal to `metamaskFeeRate` when no discount
* applied. Use it to show the original fee next to the discounted one;
* `metamaskFeeRate` is the rate the order is charged.
*
* Absent under the same rule as `feeSource`.
*/
undiscountedMetamaskFeeRate?: number;

// Optional detailed breakdown for transparency
breakdown?: {
baseFeeRate: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,8 @@ export function quantizeBuilderFeeTenthsBps(discountBips: number): number {
* report one. Distinguishes a genuine zero (a TWAP, for instance) from the zero
* a concurrent fully-waived submit leaves in provider state.
* @returns The quote with its MetaMask component and totals re-priced, tagged
* with the fee source they were priced from.
* with the fee source they were priced from, the discount that source applied,
* and the MetaMask fee rate before that discount.
*/
export function applyFeeResolution(params: {
fees: FeeCalculationResult;
Expand Down Expand Up @@ -412,6 +413,10 @@ export function applyFeeResolution(params: {
// fill rather than the unfloored fraction the discount implies.
const metamaskFeeRate =
quantizeBuilderFeeTenthsBps(discountBips) / BUILDER_FEE_TENTHS_BPS_PER_UNIT;
// Same expression with no discount, so the two rates are equal whenever the
// default source wins.
const undiscountedMetamaskFeeRate =
quantizeBuilderFeeTenthsBps(0) / BUILDER_FEE_TENTHS_BPS_PER_UNIT;
const parsedAmount =
amount === undefined ? undefined : Number.parseFloat(amount);
// A non-positive notional is not an order size, and recomputing from it would
Expand All @@ -432,6 +437,8 @@ export function applyFeeResolution(params: {
metamaskFeeRate,
feeRate,
feeSource: resolution.source,
metamaskFeeDiscountBips: discountBips,
undiscountedMetamaskFeeRate,
...(notional !== undefined && {
metamaskFeeAmount: notional * metamaskFeeRate,
feeAmount: notional * feeRate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,9 @@ describe('applyFeeResolution', () => {

expect(priced.metamaskFeeRate).toBe(0.001);
expect(priced.metamaskFeeAmount).toBe(1);
// Nor does it inherit the waiver's discount.
expect(priced.metamaskFeeDiscountBips).toBe(0);
expect(priced.undiscountedMetamaskFeeRate).toBe(0.001);
});

it('re-prices rates without amounts when no notional was supplied', () => {
Expand All @@ -597,4 +600,104 @@ describe('applyFeeResolution', () => {
// The previously quoted amounts are left as the provider reported them.
expect(priced.metamaskFeeAmount).toBe(fees.metamaskFeeAmount);
});

it('reports a 0-bip discount and an undiscounted rate equal to the quoted rate when the default source won', () => {
// The provider rate carries a discount a concurrent submit left behind, so
// the undiscounted rate must not be read from it.
const priced = applyFeeResolution({
fees: { ...fees, metamaskFeeRate: 0.0005, feeRate: 0.00095 },
resolution: {
feeBips: 10,
discountBips: undefined,
source: 'default',
subscription: createStatus({ eligible: false, reason: 'no-source' }),
},
amount: '1000',
});

expect(priced.feeSource).toBe('default');
expect(priced.metamaskFeeDiscountBips).toBe(0);
expect(priced.undiscountedMetamaskFeeRate).toBe(0.001);
expect(priced.undiscountedMetamaskFeeRate).toBe(priced.metamaskFeeRate);
});

it('reports the rewards discount and the undiscounted rate it was taken off', () => {
// 3333 bips off the 10-bip default is 6.667 bips, which the venue floors to
// 6.6: the quoted rate is undiscounted x (1 - d), floored to a tenth of a bip.
const priced = applyFeeResolution({
fees,
resolution: {
feeBips: 6.667,
discountBips: 3333,
source: 'rewards',
subscription: createStatus({ eligible: false, reason: 'no-source' }),
},
amount: '1000',
});

expect(priced.feeSource).toBe('rewards');
expect(priced.metamaskFeeDiscountBips).toBe(3333);
expect(priced.undiscountedMetamaskFeeRate).toBe(0.001);
expect(priced.metamaskFeeRate).toBe(0.00066);
});

it('reports a 0-bip discount when rewards wins a tie with the default', () => {
const priced = applyFeeResolution({
fees,
resolution: {
feeBips: 10,
discountBips: 0,
source: 'rewards',
subscription: createStatus({ eligible: false, reason: 'no-source' }),
},
amount: '1000',
});

expect(priced.feeSource).toBe('rewards');
expect(priced.metamaskFeeDiscountBips).toBe(0);
expect(priced.metamaskFeeRate).toBe(priced.undiscountedMetamaskFeeRate);
});

it('reports the subscription discount at the rate a full waiver is charged', () => {
const priced = applyFeeResolution({
fees,
resolution: {
feeBips: 0,
discountBips: 10000,
source: 'subscription',
subscription: createStatus(),
subscriptionWaiverKind: 'full',
},
amount: '1000',
});

expect(priced.feeSource).toBe('subscription');
expect(priced.metamaskFeeDiscountBips).toBe(10000);
expect(priced.undiscountedMetamaskFeeRate).toBe(0.001);
expect(priced.metamaskFeeRate).toBe(0);
});

it('reports the subscription discount at the rate a partial waiver is charged', () => {
// 333 of a 1000 USD order covered: 6.67 bips blended, a 3330-bip discount.
const priced = applyFeeResolution({
fees,
resolution: {
feeBips: 6.67,
discountBips: 3330,
source: 'subscription',
subscription: createStatus({ remainingNotionalUsd: 333 }),
subscriptionWaiverKind: 'partial',
},
amount: '1000',
});

expect(priced.feeSource).toBe('subscription');
expect(priced.metamaskFeeDiscountBips).toBe(3330);
expect(priced.undiscountedMetamaskFeeRate).toBe(0.001);
// The submit path charges quantizeBuilderFeeTenthsBps(d) tenths of a bip.
expect(priced.metamaskFeeRate).toBe(
quantizeBuilderFeeTenthsBps(3330) / 100_000,
);
expect(priced.metamaskFeeRate).toBe(0.00066);
});
});
Loading