Skip to content

Commit 14fd5f2

Browse files
fix: not setting default slippage for non stablecoin pairs (#14730)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> This PR fixes an issue where if you were on a stablecoin pair then selected a pair of tokens which were both not stablecoins, we would be keeping the default slippage as 0.5%. This will set the default slippage to 2% for non stablecoin pairs. ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to Swaps 2. Select USDC and USDT as the pair 3. See slippage be set to 0.5% 4. Change USDC to ETH 5. See slippage be set to 2% ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> https://github.com/user-attachments/assets/f0a67e3d-acbf-4ab9-a0a7-2860e175027f ## **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 - [ ] 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.
1 parent 710d7c3 commit 14fd5f2

File tree

4 files changed

+233
-50
lines changed

4 files changed

+233
-50
lines changed

CHANGELOG.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- fix(swaps): set default slippage when source or destination token is not stablecoin ([#14730](https://github.com/MetaMask/metamask-mobile/pull/14730))
13+
1014
### Changed
1115

1216
- fix(multi-srp): display errors only after all the words are have been entered ([#14607](https://github.com/MetaMask/metamask-mobile/pull/14607))
@@ -103,11 +107,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
103107
- fix: inherit icon size from text component parent ([#14024](https://github.com/MetaMask/metamask-mobile/pull/14024))
104108
- fix: animation added for loading state on SnapUILink button ([#13973](https://github.com/MetaMask/metamask-mobile/pull/13973))
105109
- fix: Revert "chore: upgrade Xcode 16 on bitrise.yml" ([#14012](https://github.com/MetaMask/metamask-mobile/pull/14012))
106-
107-
### Fixed
108-
109110
- fix(bridge): hide staked native assets from token selectors ([#14457](https://github.com/MetaMask/metamask-mobile/pull/14457))
110111

112+
111113
## [7.43.0]
112114

113115
### Added

app/components/UI/Swaps/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -595,9 +595,11 @@ function SwapsAmountView({
595595
destinationTokenAddress,
596596
);
597597
if (enableDirectWrapping && !isDirectWrapping) {
598+
// ETH <> WETH, set slippage to 0
598599
setSlippage(0);
599600
setIsDirectWrapping(true);
600601
} else if (isDirectWrapping && !enableDirectWrapping) {
602+
// Coming out of ETH <> WETH to a non (ETH <> WETH) pair, reset slippage
601603
setSlippage(AppConstants.SWAPS.DEFAULT_SLIPPAGE);
602604
setIsDirectWrapping(false);
603605
}

app/components/UI/Swaps/useStablecoinsDefaultSlippage.test.tsx

+131-33
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { renderHookWithProvider } from '../../../util/test/renderWithProvider';
2-
import { useStablecoinsDefaultSlippage } from './useStablecoinsDefaultSlippage';
2+
import { useStablecoinsDefaultSlippage, handleStablecoinSlippage } from './useStablecoinsDefaultSlippage';
33
import { Hex } from '@metamask/utils';
44
import { swapsUtils } from '@metamask/swaps-controller';
5+
import AppConstants from '../../../core/AppConstants';
56

67
describe('useStablecoinsDefaultSlippage', () => {
78
const mockSetSlippage = jest.fn();
@@ -79,7 +80,7 @@ describe('useStablecoinsDefaultSlippage', () => {
7980
renderHookWithProvider(
8081
() =>
8182
useStablecoinsDefaultSlippage({
82-
sourceTokenAddress: '0x6b175474e89094c44da98b954eedeac495271d0f', // DAI (not in the list)
83+
sourceTokenAddress: '0x123', // Non-stablecoin
8384
destTokenAddress: '0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT
8485
chainId: swapsUtils.ETH_CHAIN_ID as Hex,
8586
setSlippage: mockSetSlippage,
@@ -95,7 +96,7 @@ describe('useStablecoinsDefaultSlippage', () => {
9596
() =>
9697
useStablecoinsDefaultSlippage({
9798
sourceTokenAddress: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC
98-
destTokenAddress: '0x6b175474e89094c44da98b954eedeac495271d0f', // DAI (not in the list)
99+
destTokenAddress: '0x123', // Non-stablecoin
99100
chainId: swapsUtils.ETH_CHAIN_ID as Hex,
100101
setSlippage: mockSetSlippage,
101102
}),
@@ -147,44 +148,141 @@ describe('useStablecoinsDefaultSlippage', () => {
147148

148149
expect(mockSetSlippage).not.toHaveBeenCalled();
149150
});
151+
});
150152

151-
it('calls setSlippage only once when dependencies change', () => {
152-
// First render with stablecoins
153-
const { rerender } = renderHookWithProvider(
154-
() =>
155-
useStablecoinsDefaultSlippage({
156-
sourceTokenAddress: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC
157-
destTokenAddress: '0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT
158-
chainId: swapsUtils.ETH_CHAIN_ID as Hex,
159-
setSlippage: mockSetSlippage,
160-
}),
161-
{ state: initialState },
162-
);
153+
describe('handleStablecoinSlippage', () => {
154+
const mockSetSlippage = jest.fn();
163155

164-
// First render should call setSlippage
165-
expect(mockSetSlippage).toHaveBeenCalledTimes(1);
156+
beforeEach(() => {
157+
jest.clearAllMocks();
158+
});
166159

167-
// Clear the mock to track subsequent calls
168-
mockSetSlippage.mockClear();
160+
it('sets stablecoin slippage when both tokens are stablecoins', () => {
161+
handleStablecoinSlippage({
162+
sourceTokenAddress: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC
163+
destTokenAddress: '0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT
164+
chainId: swapsUtils.ETH_CHAIN_ID as Hex,
165+
setSlippage: mockSetSlippage,
166+
});
169167

170-
// Rerender with the same props should not call setSlippage again
171-
rerender({});
168+
expect(mockSetSlippage).toHaveBeenCalledWith(AppConstants.SWAPS.DEFAULT_SLIPPAGE_STABLECOINS);
169+
});
170+
171+
it('does not set slippage when source token is not on the list of stablecoins', () => {
172+
handleStablecoinSlippage({
173+
sourceTokenAddress: '0x123', // Non-stablecoin
174+
destTokenAddress: '0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT
175+
chainId: swapsUtils.ETH_CHAIN_ID as Hex,
176+
setSlippage: mockSetSlippage,
177+
});
172178

173179
expect(mockSetSlippage).not.toHaveBeenCalled();
180+
});
174181

175-
// Create a new hook instance with different props
176-
renderHookWithProvider(
177-
() =>
178-
useStablecoinsDefaultSlippage({
179-
sourceTokenAddress: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC
180-
destTokenAddress: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', // USDC.e on Polygon
181-
chainId: swapsUtils.ETH_CHAIN_ID as Hex,
182-
setSlippage: mockSetSlippage,
183-
}),
184-
{ state: initialState },
185-
);
182+
it('does not set slippage when destination token is not on the list of stablecoins', () => {
183+
handleStablecoinSlippage({
184+
sourceTokenAddress: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC
185+
destTokenAddress: '0x123', // Non-stablecoin
186+
chainId: swapsUtils.ETH_CHAIN_ID as Hex,
187+
setSlippage: mockSetSlippage,
188+
});
189+
190+
expect(mockSetSlippage).not.toHaveBeenCalled();
191+
});
192+
193+
it('does not set slippage when chain ID is not supported', () => {
194+
handleStablecoinSlippage({
195+
sourceTokenAddress: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC
196+
destTokenAddress: '0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT
197+
chainId: '0x9999' as Hex, // Unsupported chain ID
198+
setSlippage: mockSetSlippage,
199+
});
200+
201+
expect(mockSetSlippage).not.toHaveBeenCalled();
202+
});
203+
204+
it('does not set slippage when source token address is missing', () => {
205+
handleStablecoinSlippage({
206+
destTokenAddress: '0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT
207+
chainId: swapsUtils.ETH_CHAIN_ID as Hex,
208+
setSlippage: mockSetSlippage,
209+
});
210+
211+
expect(mockSetSlippage).not.toHaveBeenCalled();
212+
});
213+
214+
it('does not set slippage when destination token address is missing', () => {
215+
handleStablecoinSlippage({
216+
sourceTokenAddress: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC
217+
chainId: swapsUtils.ETH_CHAIN_ID as Hex,
218+
setSlippage: mockSetSlippage,
219+
});
220+
221+
expect(mockSetSlippage).not.toHaveBeenCalled();
222+
});
223+
224+
it('resets slippage to default when transitioning from stablecoin pair to non-stablecoin pair', () => {
225+
handleStablecoinSlippage({
226+
sourceTokenAddress: '0x123', // Non-stablecoin
227+
destTokenAddress: '0x456', // Non-stablecoin
228+
chainId: swapsUtils.ETH_CHAIN_ID as Hex,
229+
setSlippage: mockSetSlippage,
230+
prevSourceTokenAddress: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC
231+
prevDestTokenAddress: '0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT
232+
});
233+
234+
expect(mockSetSlippage).toHaveBeenCalledWith(AppConstants.SWAPS.DEFAULT_SLIPPAGE);
235+
});
236+
237+
it('does not reset slippage when transitioning from non-stablecoin pair to another non-stablecoin pair', () => {
238+
handleStablecoinSlippage({
239+
sourceTokenAddress: '0x6b175474e89094c44da98b954eedeac495271d0f', // DAI
240+
destTokenAddress: '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599', // WBTC
241+
chainId: swapsUtils.ETH_CHAIN_ID as Hex,
242+
setSlippage: mockSetSlippage,
243+
prevSourceTokenAddress: '0x123', // Non-stablecoin
244+
prevDestTokenAddress: '0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT
245+
});
246+
247+
expect(mockSetSlippage).not.toHaveBeenCalled();
248+
});
249+
250+
it('sets default slippage when transitioning from non-stablecoin pair to stablecoin pair', () => {
251+
handleStablecoinSlippage({
252+
sourceTokenAddress: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC
253+
destTokenAddress: '0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT
254+
chainId: swapsUtils.ETH_CHAIN_ID as Hex,
255+
setSlippage: mockSetSlippage,
256+
prevSourceTokenAddress: '0x123', // Non-stablecoin
257+
prevDestTokenAddress: '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599', // WBTC
258+
});
259+
260+
expect(mockSetSlippage).toHaveBeenCalledWith(AppConstants.SWAPS.DEFAULT_SLIPPAGE_STABLECOINS);
261+
});
262+
263+
it('handles transition from stablecoin pair to missing token addresses', () => {
264+
handleStablecoinSlippage({
265+
sourceTokenAddress: undefined,
266+
destTokenAddress: undefined,
267+
chainId: swapsUtils.ETH_CHAIN_ID as Hex,
268+
setSlippage: mockSetSlippage,
269+
prevSourceTokenAddress: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC
270+
prevDestTokenAddress: '0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT
271+
});
272+
273+
expect(mockSetSlippage).not.toHaveBeenCalled();
274+
});
275+
276+
it('does not reset slippage when previous token addresses are missing', () => {
277+
handleStablecoinSlippage({
278+
sourceTokenAddress: '0x123', // Non-stablecoin
279+
destTokenAddress: '0x456', // Non-stablecoin
280+
chainId: swapsUtils.ETH_CHAIN_ID as Hex,
281+
setSlippage: mockSetSlippage,
282+
prevSourceTokenAddress: undefined,
283+
prevDestTokenAddress: undefined,
284+
});
186285

187-
// Should not call setSlippage because tokens are on different chains
188286
expect(mockSetSlippage).not.toHaveBeenCalled();
189287
});
190288
});

app/components/UI/Swaps/useStablecoinsDefaultSlippage.ts

+95-14
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import AppConstants from '../../../core/AppConstants';
33
import { Hex } from '@metamask/utils';
44
import { swapsUtils } from '@metamask/swaps-controller';
55
import { toChecksumHexAddress } from '@metamask/controller-utils';
6+
import usePrevious from '../../hooks/usePrevious';
67

78
// USDC and USDT for now
89
const StablecoinsByChainId: Partial<Record<Hex, Set<string>>> = {
@@ -49,6 +50,81 @@ const StablecoinsByChainId: Partial<Record<Hex, Set<string>>> = {
4950
]),
5051
};
5152

53+
/**
54+
* This function checks if the source and destination tokens are both stablecoins.
55+
* @param sourceTokenAddress - The address of the source token.
56+
* @param destTokenAddress - The address of the destination token.
57+
* @param chainId - The chain id of the swap.
58+
* @returns true if the source and destination tokens are both stablecoins, false otherwise.
59+
*/
60+
const getIsStablecoinPair = (
61+
sourceTokenAddress: string,
62+
destTokenAddress: string,
63+
chainId: Hex,
64+
) => {
65+
const stablecoins = StablecoinsByChainId[chainId];
66+
67+
if (!stablecoins) return false;
68+
69+
return (
70+
(stablecoins.has(sourceTokenAddress.toLowerCase()) ||
71+
stablecoins.has(toChecksumHexAddress(sourceTokenAddress))) &&
72+
(stablecoins.has(destTokenAddress.toLowerCase()) ||
73+
stablecoins.has(toChecksumHexAddress(destTokenAddress)))
74+
);
75+
};
76+
77+
/**
78+
* This function handles the slippage for stablecoins swaps.
79+
* It checks if the source and destination tokens are both stablecoins and if so,
80+
* it sets the slippage to 0.5%.
81+
* @param sourceTokenAddress - The address of the source token.
82+
* @param destTokenAddress - The address of the destination token.
83+
* @param chainId - The chain id of the swap.
84+
* @param setSlippage - The function to set the slippage.
85+
* @param prevSourceTokenAddress - The previous source token address.
86+
* @param prevDestTokenAddress - The previous destination token address.
87+
*/
88+
export const handleStablecoinSlippage = ({
89+
sourceTokenAddress,
90+
destTokenAddress,
91+
chainId,
92+
setSlippage,
93+
prevSourceTokenAddress,
94+
prevDestTokenAddress,
95+
}: {
96+
sourceTokenAddress?: string;
97+
destTokenAddress?: string;
98+
chainId: Hex;
99+
setSlippage: (slippage: number) => void;
100+
prevSourceTokenAddress?: string;
101+
prevDestTokenAddress?: string;
102+
}) => {
103+
if (!sourceTokenAddress || !destTokenAddress) return;
104+
105+
const isStablecoinPair = getIsStablecoinPair(
106+
sourceTokenAddress,
107+
destTokenAddress,
108+
chainId,
109+
);
110+
111+
if (isStablecoinPair) {
112+
setSlippage(AppConstants.SWAPS.DEFAULT_SLIPPAGE_STABLECOINS);
113+
}
114+
115+
if (!prevSourceTokenAddress || !prevDestTokenAddress) return;
116+
117+
const prevIsStablecoinPair = getIsStablecoinPair(
118+
prevSourceTokenAddress,
119+
prevDestTokenAddress,
120+
chainId,
121+
);
122+
123+
if (prevIsStablecoinPair && !isStablecoinPair) {
124+
setSlippage(AppConstants.SWAPS.DEFAULT_SLIPPAGE);
125+
}
126+
};
127+
52128
/**
53129
* This hook is used to update the slippage for stablecoins swaps.
54130
* It checks if the source and destination tokens are both stablecoins and if so,
@@ -69,19 +145,24 @@ export const useStablecoinsDefaultSlippage = ({
69145
chainId: Hex;
70146
setSlippage: (slippage: number) => void;
71147
}) => {
72-
useEffect(() => {
73-
const stablecoins = StablecoinsByChainId[chainId];
148+
const prevSourceTokenAddress = usePrevious(sourceTokenAddress);
149+
const prevDestTokenAddress = usePrevious(destTokenAddress);
74150

75-
if (
76-
stablecoins &&
77-
sourceTokenAddress &&
78-
destTokenAddress &&
79-
(stablecoins.has(sourceTokenAddress.toLowerCase()) ||
80-
stablecoins.has(toChecksumHexAddress(sourceTokenAddress))) &&
81-
(stablecoins.has(destTokenAddress.toLowerCase()) ||
82-
stablecoins.has(toChecksumHexAddress(destTokenAddress)))
83-
) {
84-
setSlippage(AppConstants.SWAPS.DEFAULT_SLIPPAGE_STABLECOINS);
85-
}
86-
}, [setSlippage, sourceTokenAddress, destTokenAddress, chainId]);
151+
useEffect(() => {
152+
handleStablecoinSlippage({
153+
sourceTokenAddress,
154+
destTokenAddress,
155+
chainId,
156+
setSlippage,
157+
prevSourceTokenAddress,
158+
prevDestTokenAddress,
159+
});
160+
}, [
161+
setSlippage,
162+
sourceTokenAddress,
163+
destTokenAddress,
164+
chainId,
165+
prevSourceTokenAddress,
166+
prevDestTokenAddress,
167+
]);
87168
};

0 commit comments

Comments
 (0)