diff --git a/apps/frontend/src/app/5_pages/ConvertPage/ConvertPage.slippage.test.ts b/apps/frontend/src/app/5_pages/ConvertPage/ConvertPage.slippage.test.ts
new file mode 100644
index 000000000..131b6c28a
--- /dev/null
+++ b/apps/frontend/src/app/5_pages/ConvertPage/ConvertPage.slippage.test.ts
@@ -0,0 +1,74 @@
+import {
+ MAXIMUM_SLIPPAGE_TOLERANCE,
+ SLIPPAGE_TOLERANCE_WARNING_THRESHOLD,
+ isHighSlippageTolerance,
+ isInvalidSlippageTolerance,
+} from './ConvertPage.slippage';
+
+describe('ConvertPage slippage tolerance policy', () => {
+ it('warns above 5% and hard-caps at 49%', () => {
+ expect(SLIPPAGE_TOLERANCE_WARNING_THRESHOLD).toBe(5);
+ expect(MAXIMUM_SLIPPAGE_TOLERANCE).toBe(49);
+ });
+
+ describe('isHighSlippageTolerance', () => {
+ it('does not warn for the default tolerance', () => {
+ expect(isHighSlippageTolerance('0.5')).toBe(false);
+ });
+
+ it('does not warn just below 5%', () => {
+ expect(isHighSlippageTolerance('4.99')).toBe(false);
+ });
+
+ it('warns at exactly 5% (catches 5 typed instead of 0.5)', () => {
+ expect(isHighSlippageTolerance('5')).toBe(true);
+ });
+
+ it('warns at 5.01%', () => {
+ expect(isHighSlippageTolerance('5.01')).toBe(true);
+ });
+
+ it('warns at 6%', () => {
+ expect(isHighSlippageTolerance('6')).toBe(true);
+ });
+
+ it('warns at 49%', () => {
+ expect(isHighSlippageTolerance('49')).toBe(true);
+ });
+
+ it('does not warn for an empty input', () => {
+ expect(isHighSlippageTolerance('')).toBe(false);
+ });
+ });
+
+ describe('isInvalidSlippageTolerance', () => {
+ it('allows the default tolerance', () => {
+ expect(isInvalidSlippageTolerance('0.5')).toBe(false);
+ });
+
+ it('allows 5.01% and 6% (warning only, swap still allowed)', () => {
+ expect(isInvalidSlippageTolerance('5.01')).toBe(false);
+ expect(isInvalidSlippageTolerance('6')).toBe(false);
+ });
+
+ it('allows exactly 49%', () => {
+ expect(isInvalidSlippageTolerance('49')).toBe(false);
+ });
+
+ it('blocks 49.01%', () => {
+ expect(isInvalidSlippageTolerance('49.01')).toBe(true);
+ });
+
+ it('blocks 50%', () => {
+ expect(isInvalidSlippageTolerance('50')).toBe(true);
+ });
+
+ it('blocks 100%', () => {
+ expect(isInvalidSlippageTolerance('100')).toBe(true);
+ });
+
+ it('allows an empty input', () => {
+ expect(isInvalidSlippageTolerance('')).toBe(false);
+ });
+ });
+});
diff --git a/apps/frontend/src/app/5_pages/ConvertPage/ConvertPage.slippage.ts b/apps/frontend/src/app/5_pages/ConvertPage/ConvertPage.slippage.ts
new file mode 100644
index 000000000..fa566a97e
--- /dev/null
+++ b/apps/frontend/src/app/5_pages/ConvertPage/ConvertPage.slippage.ts
@@ -0,0 +1,14 @@
+// Slippage tolerance input policy. Not to be confused with
+// MAXIMUM_ALLOWED_SLIPPAGE in ConvertPage.constants.ts, which gates the
+// quoted price impact of a swap, not the user-entered tolerance.
+export const SLIPPAGE_TOLERANCE_WARNING_THRESHOLD = 5;
+export const MAXIMUM_SLIPPAGE_TOLERANCE = 49;
+
+// Inclusive: exactly 5 also warns, to catch 5 typed instead of 0.5.
+export const isHighSlippageTolerance = (value: string) =>
+ Number(value) >= SLIPPAGE_TOLERANCE_WARNING_THRESHOLD;
+
+// The input's HTML max attribute does not block typed values, so values
+// above the cap must also be rejected here and block submission.
+export const isInvalidSlippageTolerance = (value: string) =>
+ Number(value) > MAXIMUM_SLIPPAGE_TOLERANCE;
diff --git a/apps/frontend/src/app/5_pages/ConvertPage/ConvertPage.tsx b/apps/frontend/src/app/5_pages/ConvertPage/ConvertPage.tsx
index 2982456a3..1869d8f68 100644
--- a/apps/frontend/src/app/5_pages/ConvertPage/ConvertPage.tsx
+++ b/apps/frontend/src/app/5_pages/ConvertPage/ConvertPage.tsx
@@ -73,6 +73,11 @@ import {
SMART_ROUTER_STABLECOINS,
SWAP_ROUTES,
} from './ConvertPage.constants';
+import {
+ MAXIMUM_SLIPPAGE_TOLERANCE,
+ isHighSlippageTolerance,
+ isInvalidSlippageTolerance,
+} from './ConvertPage.slippage';
import { CategoryType } from './ConvertPage.types';
import { AssetDropdownWithFilters } from './components/AssetDropdownWithFilters/AssetDropdownWithFilters';
import { useConversionMaintenance } from './hooks/useConversionMaintenance';
@@ -183,6 +188,16 @@ const ConvertPage: FC = () => {
DEFAULT_SLIPPAGE_TOLERANCE,
);
+ const isSlippageToleranceInvalid = useMemo(
+ () => isInvalidSlippageTolerance(slippageTolerance),
+ [slippageTolerance],
+ );
+
+ const isSlippageToleranceHigh = useMemo(
+ () => isHighSlippageTolerance(slippageTolerance),
+ [slippageTolerance],
+ );
+
const [priceInQuote, setPriceQuote] = useState(false);
const hasMyntBalance = useMemo(() => myntBalance.gt(0), [myntBalance]);
const [amount, setAmount, weiAmount] = useWeiAmountInput('');
@@ -539,6 +554,7 @@ const ConvertPage: FC = () => {
Number(amount) > Number(maximumAmountToConvert) ||
!destinationToken ||
!route ||
+ isSlippageToleranceInvalid ||
(isSlippageHigh && !slippageWarningAccepted),
[
isInMaintenance,
@@ -547,6 +563,7 @@ const ConvertPage: FC = () => {
maximumAmountToConvert,
destinationToken,
route,
+ isSlippageToleranceInvalid,
isSlippageHigh,
slippageWarningAccepted,
],
@@ -867,8 +884,30 @@ const ConvertPage: FC = () => {
step="0.01"
decimalPrecision={2}
placeholder="0"
- max="100"
+ max={MAXIMUM_SLIPPAGE_TOLERANCE}
+ invalid={isSlippageToleranceInvalid}
/>
+
+ {isSlippageToleranceInvalid ? (
+
+ ) : (
+ isSlippageToleranceHigh && (
+
+ )
+ )}
diff --git a/apps/frontend/src/locales/en/translations.json b/apps/frontend/src/locales/en/translations.json
index 44343c9a0..642803008 100644
--- a/apps/frontend/src/locales/en/translations.json
+++ b/apps/frontend/src/locales/en/translations.json
@@ -432,7 +432,9 @@
"quoteError": "No route found, please try another asset or a lower amount",
"noAvailableTokens": "No available tokens",
"swapValueWarning": "The swap value is significantly lower than expected due to market conditions or slippage.",
- "slippageWarning": "I acknowledge the risk and confirm to proceed"
+ "slippageWarning": "I acknowledge the risk and confirm to proceed",
+ "highSlippageToleranceWarning": "High slippage tolerance: your transaction may be frontrun and result in an unfavorable trade",
+ "invalidSlippageToleranceError": "Slippage tolerance must be {{max}}% or less"
},
"txDialog": {
"convertTitle": "Convert {{from}} to {{to}}",