Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enhanced gas and amount validation #951

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
103 changes: 65 additions & 38 deletions lib/handlers/quote/schema/quote-schema.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,64 @@
import BaseJoi from '@hapi/joi'
import { SUPPORTED_CHAINS } from '../../injector-sor'

const Joi = BaseJoi.extend((joi) => ({
base: joi.array(),
type: 'stringArray',
// Constants for amount validation
export const MAX_UINT256 = BigInt('115792089237316195423570985008687907853269984665640564039457584007913129639935');
const MAX_SAFE_AMOUNT_LENGTH = MAX_UINT256.toString().length;

// Joi extension for handling BigInt validation
const createBigIntExtension = (joi: any) => ({
type: 'bigInt',
base: joi.string(),
messages: {
'stringArray.type': '{{#label}} is not a valid string array',
'amount.required': '{{#label}} is required',
'amount.numeric': '{{#label}} must contain only numeric characters',
'amount.positive': '{{#label}} must be greater than 0',
'amount.exceedsMaxUint256': '{{#label}} exceeds maximum uint256 value',
'amount.invalid': '{{#label}} is not a valid number'
},
coerce: (value, helpers) => {
if (typeof value !== 'string') {
return { value: value, errors: [helpers.error('stringArray.type')] }
validate(value: string, helpers: any) {
if (!value) {
return helpers.error('amount.required');
}
value = value.replace(/^\[|\]$/g, '').split(',')
const ar = (value as string[]).map((val) => {
return val.trim()
})
return { value: ar }
},
}))

if (!new RegExp(`^[0-9]{1,${MAX_SAFE_AMOUNT_LENGTH}}$`).test(value)) {
return helpers.error('amount.numeric');
}

try {
const amountBN = BigInt(value);
if (amountBN <= 0n) {
return helpers.error('amount.positive');
}
if (amountBN > MAX_UINT256) {
return helpers.error('amount.exceedsMaxUint256');
}
return value;
} catch (error) {
return helpers.error('amount.invalid');
}
}
});

const Joi = BaseJoi
.extend((joi) => ({
base: joi.array(),
type: 'stringArray',
messages: {
'stringArray.type': '{{#label}} is not a valid string array',
},
coerce: (value, helpers) => {
if (typeof value !== 'string') {
return { value: value, errors: [helpers.error('stringArray.type')] }
}
value = value.replace(/^\[|\]$/g, '').split(',')
const ar = (value as string[]).map((val) => {
return val.trim()
})
return { value: ar }
},
}))
.extend(createBigIntExtension);

export const QuoteQueryParamsJoi = Joi.object({
tokenInAddress: Joi.string().alphanum().max(42).required(),
Expand All @@ -28,21 +69,15 @@ export const QuoteQueryParamsJoi = Joi.object({
tokenOutChainId: Joi.number()
.valid(...SUPPORTED_CHAINS.values())
.required(),
amount: Joi.string()
.pattern(/^[0-9]+$/)
.max(77) // TODO: validate < 2**256
.required(),
amount: Joi.bigInt().required(),
type: Joi.string().valid('exactIn', 'exactOut').required(),
recipient: Joi.string()
.pattern(new RegExp(/^0x[a-fA-F0-9]{40}$/))
.optional(),
slippageTolerance: Joi.number().min(0).max(20).precision(2).optional(),
deadline: Joi.number().max(10800).optional(), // 180 mins, same as interface max
algorithm: Joi.string().valid('alpha', 'legacy').optional(),
gasPriceWei: Joi.string()
.pattern(/^[0-9]+$/)
.max(30)
.optional(),
gasPriceWei: Joi.bigInt().optional(),
minSplits: Joi.number().max(7).optional(),
forceCrossProtocol: Joi.boolean().optional(),
forceMixedRoutes: Joi.boolean().optional(),
Expand All @@ -51,28 +86,20 @@ export const QuoteQueryParamsJoi = Joi.object({
permitSignature: Joi.string().optional(),
permitNonce: Joi.string().optional(),
permitExpiration: Joi.number().optional(),
permitAmount: Joi.string()
.pattern(/^[0-9]+$/)
.max(77),
permitAmount: Joi.bigInt().optional(),
permitSigDeadline: Joi.number().optional(),
// TODO: Remove once universal router is no longer behind a feature flag.
enableUniversalRouter: Joi.boolean().optional().default(false),
quoteSpeed: Joi.string().valid('fast', 'standard').optional().default('standard'),
debugRoutingConfig: Joi.string().optional(),
unicornSecret: Joi.string().optional(),
intent: Joi.string().valid('quote', 'swap', 'caching', 'pricing').optional().default('quote'),
enableFeeOnTransferFeeFetching: Joi.boolean().optional().default(false),
portionBips: Joi.string()
.pattern(/^[0-9]+$/)
.max(5) // portionBips is a string type with the expectation of being parsable to integer between 0 and 10000
.optional(),
portionAmount: Joi.string()
.pattern(/^[0-9]+$/)
.optional(),
portionRecipient: Joi.string().alphanum().max(42).optional(),
source: Joi.string().max(20).optional(),
gasToken: Joi.string().alphanum().max(42).optional(),
})
enableFeeOnTransferFeeFetching: Joi.boolean().optional(),
portionBips: Joi.number().optional(),
portionAmount: Joi.string().optional(),
portionRecipient: Joi.string().optional(),
source: Joi.string().optional(),
gasToken: Joi.string().optional(),
});

// Future work: this TradeTypeParam can be converted into an enum and used in the
// schema above and in the route QuoteHandler.
Expand Down
Loading