diff --git a/src/domain/messages/entities/message-confirmation.entity.spec.ts b/src/domain/messages/entities/message-confirmation.entity.spec.ts index e2b7a92a9f..0fef17149f 100644 --- a/src/domain/messages/entities/message-confirmation.entity.spec.ts +++ b/src/domain/messages/entities/message-confirmation.entity.spec.ts @@ -58,6 +58,11 @@ describe('MessageConfirmationSchema', () => { message: 'Invalid "0x" notated hex string', path: ['signature'], }, + { + code: 'custom', + message: 'Invalid signature', + path: ['signature'], + }, ]), ); }); diff --git a/src/domain/messages/entities/message-confirmation.entity.ts b/src/domain/messages/entities/message-confirmation.entity.ts index 87799d0df3..5b35aa873b 100644 --- a/src/domain/messages/entities/message-confirmation.entity.ts +++ b/src/domain/messages/entities/message-confirmation.entity.ts @@ -1,6 +1,6 @@ import { SignatureType } from '@/domain/common/entities/signature-type.entity'; import { AddressSchema } from '@/validation/entities/schemas/address.schema'; -import { HexSchema } from '@/validation/entities/schemas/hex.schema'; +import { SignatureLikeSchema } from '@/validation/entities/schemas/signature.schema'; import { z } from 'zod'; export type MessageConfirmation = z.infer; @@ -9,6 +9,6 @@ export const MessageConfirmationSchema = z.object({ created: z.coerce.date(), modified: z.coerce.date(), owner: AddressSchema, - signature: HexSchema, + signature: SignatureLikeSchema, signatureType: z.nativeEnum(SignatureType), }); diff --git a/src/domain/safe/entities/multisig-transaction.entity.ts b/src/domain/safe/entities/multisig-transaction.entity.ts index cdeaae6b1e..75c2acbbcb 100644 --- a/src/domain/safe/entities/multisig-transaction.entity.ts +++ b/src/domain/safe/entities/multisig-transaction.entity.ts @@ -7,7 +7,7 @@ import { HexSchema } from '@/validation/entities/schemas/hex.schema'; import { NumericStringSchema } from '@/validation/entities/schemas/numeric-string.schema'; import { z } from 'zod'; import { CoercedNumberSchema } from '@/validation/entities/schemas/coerced-number.schema'; -import { SignatureSchema } from '@/validation/entities/schemas/signature.schema'; +import { SignatureLikeSchema } from '@/validation/entities/schemas/signature.schema'; export type Confirmation = z.infer; @@ -18,7 +18,7 @@ export const ConfirmationSchema = z.object({ submissionDate: z.coerce.date(), transactionHash: HexSchema.nullish().default(null), signatureType: z.nativeEnum(SignatureType), - signature: SignatureSchema.nullish().default(null), + signature: SignatureLikeSchema.nullish().default(null), }); export const MultisigTransactionSchema = z.object({ diff --git a/src/routes/transactions/__tests__/controllers/get-transaction-by-id.transactions.controller.spec.ts b/src/routes/transactions/__tests__/controllers/get-transaction-by-id.transactions.controller.spec.ts index edfd350a5b..339d38e452 100644 --- a/src/routes/transactions/__tests__/controllers/get-transaction-by-id.transactions.controller.spec.ts +++ b/src/routes/transactions/__tests__/controllers/get-transaction-by-id.transactions.controller.spec.ts @@ -1330,7 +1330,7 @@ describe('Get by id - Transactions Controller (Unit)', () => { signers, safe, }); - multisigTransaction.confirmations![0].signature = `0xdeadbeef`; + multisigTransaction.confirmations![0].signature = `0xdeadbee`; const getSafeUrl = `${chain.transactionService}/api/v1/safes/${safe.address}`; const getChainUrl = `${safeConfigUrl}/api/v1/chains/${chain.chainId}`; const getMultisigTransactionUrl = `${chain.transactionService}/api/v1/multisig-transactions/${multisigTransaction.safeTxHash}/`; diff --git a/src/routes/transactions/__tests__/controllers/list-queued-transactions-by-safe.transactions.controller.spec.ts b/src/routes/transactions/__tests__/controllers/list-queued-transactions-by-safe.transactions.controller.spec.ts index 1ee219b79b..d7c57c5ff2 100644 --- a/src/routes/transactions/__tests__/controllers/list-queued-transactions-by-safe.transactions.controller.spec.ts +++ b/src/routes/transactions/__tests__/controllers/list-queued-transactions-by-safe.transactions.controller.spec.ts @@ -986,7 +986,7 @@ describe('List queued transactions by Safe - Transactions Controller (Unit)', () }); }; const nonce1 = await getTransaction(1); - nonce1.confirmations![0].signature = '0xdeadbeef'; + nonce1.confirmations![0].signature = '0xdeadbee'; const nonce2 = await getTransaction(2); const transactions: Array = [ multisigToJson(nonce1) as MultisigTransaction, diff --git a/src/validation/entities/schemas/__tests__/signature.schema.spec.ts b/src/validation/entities/schemas/__tests__/signature.schema.spec.ts index 0b5814c5d7..8fdae0080d 100644 --- a/src/validation/entities/schemas/__tests__/signature.schema.spec.ts +++ b/src/validation/entities/schemas/__tests__/signature.schema.spec.ts @@ -1,5 +1,8 @@ import { faker } from '@faker-js/faker'; -import { SignatureSchema } from '@/validation/entities/schemas/signature.schema'; +import { + SignatureLikeSchema, + SignatureSchema, +} from '@/validation/entities/schemas/signature.schema'; describe('SignatureSchema', () => { it('should validate a signature', () => { @@ -57,3 +60,51 @@ describe('SignatureSchema', () => { ]); }); }); + +describe('SignatureLikeSchema', () => { + it('should validate a signature', () => { + const signature = faker.string.hexadecimal({ + // Somewhat "standard" length for dynamic signature + length: 130 + 64, + }) as `0x${string}`; + + const result = SignatureLikeSchema.safeParse(signature); + + expect(result.success).toBe(true); + }); + + it('should not validate a non-hex signature', () => { + const signature = faker.string.alphanumeric() as `0x${string}`; + + const result = SignatureLikeSchema.safeParse(signature); + + expect(!result.success && result.error.issues).toStrictEqual([ + { + code: 'custom', + message: 'Invalid "0x" notated hex string', + path: [], + }, + { + code: 'custom', + message: 'Invalid signature', + path: [], + }, + ]); + }); + + it('should not validate a incorrect length signature', () => { + const signature = faker.string.hexadecimal({ + length: 129, + }) as `0x${string}`; + + const result = SignatureLikeSchema.safeParse(signature); + + expect(!result.success && result.error.issues).toStrictEqual([ + { + code: 'custom', + message: 'Invalid signature', + path: [], + }, + ]); + }); +}); diff --git a/src/validation/entities/schemas/signature.schema.ts b/src/validation/entities/schemas/signature.schema.ts index c82f972d46..49cccc425f 100644 --- a/src/validation/entities/schemas/signature.schema.ts +++ b/src/validation/entities/schemas/signature.schema.ts @@ -1,5 +1,8 @@ import { HexSchema } from '@/validation/entities/schemas/hex.schema'; +// This does not take dynamic parts into account but we can safely +// apply it to proposed signatures as we do not support contract +// signatures in the inferface function isSignature(value: `0x${string}`): boolean { // We accept proposals of singular or concatenated signatures return (value.length - 2) % 130 === 0; @@ -8,3 +11,14 @@ function isSignature(value: `0x${string}`): boolean { export const SignatureSchema = HexSchema.refine(isSignature, { message: 'Invalid signature', }); + +// As indexed signatures may be contract signatures, we need to assume +// that signatures from our API may have a dynamic part meaning that +// we can only check that the length is "byte-aligned" +function isSignatureLike(value: `0x${string}`): boolean { + return value.length % 2 === 0; +} + +export const SignatureLikeSchema = HexSchema.refine(isSignatureLike, { + message: 'Invalid signature', +});