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
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
14 changes: 14 additions & 0 deletions apps/frontend/src/app/5_pages/ConvertPage/ConvertPage.slippage.ts
Original file line number Diff line number Diff line change
@@ -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;
41 changes: 40 additions & 1 deletion apps/frontend/src/app/5_pages/ConvertPage/ConvertPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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('');
Expand Down Expand Up @@ -539,6 +554,7 @@ const ConvertPage: FC = () => {
Number(amount) > Number(maximumAmountToConvert) ||
!destinationToken ||
!route ||
isSlippageToleranceInvalid ||
(isSlippageHigh && !slippageWarningAccepted),
[
isInMaintenance,
Expand All @@ -547,6 +563,7 @@ const ConvertPage: FC = () => {
maximumAmountToConvert,
destinationToken,
route,
isSlippageToleranceInvalid,
isSlippageHigh,
slippageWarningAccepted,
],
Expand Down Expand Up @@ -867,8 +884,30 @@ const ConvertPage: FC = () => {
step="0.01"
decimalPrecision={2}
placeholder="0"
max="100"
max={MAXIMUM_SLIPPAGE_TOLERANCE}
invalid={isSlippageToleranceInvalid}
/>

{isSlippageToleranceInvalid ? (
<ErrorBadge
level={ErrorLevel.Critical}
message={t(
pageTranslations.form.invalidSlippageToleranceError,
{ max: MAXIMUM_SLIPPAGE_TOLERANCE },
)}
dataAttribute="convert-slippage-tolerance-error"
/>
) : (
isSlippageToleranceHigh && (
<ErrorBadge
level={ErrorLevel.Warning}
message={t(
pageTranslations.form.highSlippageToleranceWarning,
)}
dataAttribute="convert-slippage-tolerance-warning"
/>
)
)}
</div>
</Accordion>

Expand Down
4 changes: 3 additions & 1 deletion apps/frontend/src/locales/en/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}}",
Expand Down