Commit b2ac9a2
authored
fix(perps): incorrect fee estimate when flipping a position (MetaMask#28013)
## **Description**
The flip position confirmation sheet showed a fee estimate ~2x lower
than the actual fee charged. A flip order is 2x position size (close
current + open opposite), but the fee estimate was calculated on 1x
notional. A secondary bug in `TradingService` applied the combined fee
rate to 2x notional for the balance check, incorrectly blocking users
with sufficient balance.
## **Changelog**
CHANGELOG entry: Fixed flip position fee estimate being ~2x lower than
actual fee charged
## **Related issues**
Fixes:
[TAT-2418](https://consensyssoftware.atlassian.net/browse/TAT-2418)
## **Manual testing steps**
```gherkin
Feature: Flip position fee estimate accuracy
Scenario: Fee estimate matches actual fee charged
Given a wallet with an open BTC long position of ~$11
When user taps Modify then selects Flip Position
Then the fee shown on the confirmation sheet
is approximately 0.09% of the full position value (2x notional)
not 0.045% (1x notional)
Scenario: User with sufficient balance is not blocked
Given a wallet with an open BTC position
And available balance is $30 (above 1x fee ~$22.50, below 2x fee ~$45)
When user attempts to flip the position
Then the flip proceeds successfully
```
## **Screenshots/Recordings**
_Evidence available in task artifacts — will be added by reviewer if
needed._
### **Before**
<!-- [screenshots/recordings] -->
<img width="1206" height="2622" alt="before-flip-fee-estimate"
src="https://github.com/user-attachments/assets/80b989b3-8ae2-4c87-ae03-610371a592e7"
/>
### **After**
<!-- [screenshots/recordings] -->
<img width="1206" height="2622" alt="after-flip-fee-estimate"
src="https://github.com/user-attachments/assets/5b9701b8-ab9d-4f82-88d5-08be1bf2c4b7"
/>
## **Pre-merge author checklist**
- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
## **Validation Recipe**
<details>
<summary>recipe.json — automated validation (14 steps)</summary>
```json
{
"pr": "28013",
"title": "Flip position fee estimate is ~2x lower than actual fee charged",
"jira": "TAT-2418",
"acceptance_criteria": [
"Fee estimate shown on flip confirmation sheet matches actual fee charged (within normal rounding tolerance)",
"Users with sufficient margin for the actual fee are not blocked from submitting a flip order",
"No new TypeScript errors"
],
"validate": {
"static": ["yarn lint:tsc"],
"runtime": {
"pre_conditions": ["wallet.unlocked", "perps.feature_enabled"],
"steps": [
{
"id": "open_pos",
"description": "Open a BTC long position to flip",
"action": "flow_ref",
"ref": "trade-open-market",
"params": { "symbol": "BTC", "side": "long", "usdAmount": "11" }
},
{
"id": "wait_position",
"description": "Wait for BTC position to appear after opening",
"action": "wait_for",
"expression": "Engine.context.PerpsController.getPositions().then(function(positions){ var btc = positions.filter(function(p){ return p.symbol === 'BTC'; }); return JSON.stringify({positions: btc}); })",
"timeout_ms": 20000,
"assert": { "operator": "length_gt", "field": "positions", "value": 0 }
},
{
"id": "nav_market_details",
"description": "Navigate to BTC market details to access flip confirmation sheet",
"action": "navigate",
"target": "PerpsMarketDetails",
"params": { "symbol": "BTC" }
},
{
"id": "wait_market_details",
"description": "Wait for market details to load",
"action": "wait_for",
"route": "PerpsMarketDetails"
},
{
"id": "wait_modify_button",
"description": "Wait for the Modify button to appear (position must be loaded)",
"action": "wait_for",
"test_id": "perps-market-details-modify-button"
},
{
"id": "press_modify",
"description": "Open the modify action sheet",
"action": "press",
"test_id": "perps-market-details-modify-button"
},
{
"id": "wait_flip_option",
"description": "Wait for the flip position option in the modify action sheet",
"action": "wait_for",
"test_id": "undefined-flip_position"
},
{
"id": "press_flip_option",
"description": "Select flip position to open flip confirmation sheet",
"action": "press",
"test_id": "undefined-flip_position"
},
{
"id": "wait_flip_sheet",
"description": "Wait for flip confirmation bottom sheet to appear",
"action": "wait_for",
"test_id": "perps-flip-position-confirm-sheet"
},
{
"id": "check_fee_calculation",
"description": "Assert the fee is calculated on 2x notional.",
"action": "eval_async",
"expression": "Engine.context.PerpsController.getPositions().then(function(positions) { var pos = null; for (var i = 0; i < positions.length; i++) { if (positions[i].symbol === 'BTC') { pos = positions[i]; break; } } if (!pos) return JSON.stringify({error: 'no BTC position'}); var size = Math.abs(parseFloat(pos.size)); var price = parseFloat(pos.entryPrice); var usdAmount1x = size * price; var usdAmount2x = size * 2 * price; return JSON.stringify({positionSize: size, entryPrice: price, fee_base_1x: usdAmount1x, fee_base_2x_correct: usdAmount2x}); })",
"assert": { "operator": "not_null" }
},
{
"id": "evidence_fee_sheet",
"description": "Capture fee estimate shown on confirmation sheet",
"action": "screenshot",
"filename": "evidence-flip-fee-estimate.png"
},
{
"id": "check_no_errors",
"description": "Verify no errors in Metro logs during flip flow",
"action": "log_watch",
"window_seconds": 5,
"must_not_appear": ["TypeError", "undefined is not an object"]
},
{
"id": "cancel_flip",
"description": "Cancel the flip sheet without submitting",
"action": "press",
"test_id": "perps-flip-position-cancel-button"
},
{
"id": "cleanup_position",
"description": "Close BTC position to leave clean state",
"action": "flow_ref",
"ref": "trade-close-position",
"params": { "symbol": "BTC" }
}
]
}
}
}
```
</details>
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Changes fee/notional math used in the flip-position UI and the
`TradingService.flipPosition` balance gate; incorrect values could
affect whether users can submit flips and what fees they expect.
>
> **Overview**
> Fixes flip-position fee math to align with how fees are actually
charged.
>
> The flip confirmation sheet now estimates fees using **2x position
notional** (close + open) and adds stable `testID`s for the sheet and
its action buttons, with a unit test asserting the `usePerpsOrderFees`
amount.
>
> `TradingService.flipPosition` updates its **available-balance fee
check** to apply the combined fee rate to **1x notional** (since the
rate already includes both legs), and adds a regression test ensuring a
flip is allowed when balance covers that corrected estimate.
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
dcf9361. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->1 parent e2bb36a commit b2ac9a2
5 files changed
Lines changed: 65 additions & 7 deletions
File tree
- app
- components/UI/Perps
- components/PerpsFlipPositionConfirmSheet
- controllers/perps/services
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
759 | 759 | | |
760 | 760 | | |
761 | 761 | | |
| 762 | + | |
| 763 | + | |
| 764 | + | |
| 765 | + | |
| 766 | + | |
| 767 | + | |
| 768 | + | |
| 769 | + | |
| 770 | + | |
| 771 | + | |
Lines changed: 18 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
| 10 | + | |
10 | 11 | | |
11 | 12 | | |
12 | 13 | | |
| |||
52 | 53 | | |
53 | 54 | | |
54 | 55 | | |
55 | | - | |
| 56 | + | |
56 | 57 | | |
57 | 58 | | |
58 | 59 | | |
59 | 60 | | |
60 | | - | |
| 61 | + | |
61 | 62 | | |
62 | 63 | | |
63 | 64 | | |
| |||
270 | 271 | | |
271 | 272 | | |
272 | 273 | | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
273 | 280 | | |
274 | 281 | | |
275 | 282 | | |
| |||
366 | 373 | | |
367 | 374 | | |
368 | 375 | | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
369 | 385 | | |
app/components/UI/Perps/components/PerpsFlipPositionConfirmSheet/PerpsFlipPositionConfirmSheet.tsx
Lines changed: 8 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
| 4 | + | |
4 | 5 | | |
5 | 6 | | |
6 | 7 | | |
| |||
67 | 68 | | |
68 | 69 | | |
69 | 70 | | |
70 | | - | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
71 | 74 | | |
72 | | - | |
| 75 | + | |
73 | 76 | | |
74 | 77 | | |
75 | 78 | | |
| |||
140 | 143 | | |
141 | 144 | | |
142 | 145 | | |
| 146 | + | |
143 | 147 | | |
144 | 148 | | |
145 | 149 | | |
| |||
150 | 154 | | |
151 | 155 | | |
152 | 156 | | |
| 157 | + | |
153 | 158 | | |
154 | 159 | | |
155 | 160 | | |
| |||
160 | 165 | | |
161 | 166 | | |
162 | 167 | | |
| 168 | + | |
163 | 169 | | |
164 | 170 | | |
165 | 171 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2070 | 2070 | | |
2071 | 2071 | | |
2072 | 2072 | | |
| 2073 | + | |
| 2074 | + | |
| 2075 | + | |
| 2076 | + | |
| 2077 | + | |
| 2078 | + | |
| 2079 | + | |
| 2080 | + | |
| 2081 | + | |
| 2082 | + | |
| 2083 | + | |
| 2084 | + | |
| 2085 | + | |
| 2086 | + | |
| 2087 | + | |
| 2088 | + | |
| 2089 | + | |
| 2090 | + | |
| 2091 | + | |
| 2092 | + | |
| 2093 | + | |
| 2094 | + | |
| 2095 | + | |
| 2096 | + | |
| 2097 | + | |
2073 | 2098 | | |
2074 | 2099 | | |
2075 | 2100 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1951 | 1951 | | |
1952 | 1952 | | |
1953 | 1953 | | |
1954 | | - | |
1955 | | - | |
| 1954 | + | |
| 1955 | + | |
| 1956 | + | |
1956 | 1957 | | |
1957 | 1958 | | |
1958 | | - | |
| 1959 | + | |
1959 | 1960 | | |
1960 | 1961 | | |
1961 | 1962 | | |
| |||
0 commit comments