From 36e8730e997c30b8302f8fb047ce88fee960e636 Mon Sep 17 00:00:00 2001 From: Sertug17 <104278804+Sertug17@users.noreply.github.com> Date: Sun, 14 Jun 2026 22:47:24 +0300 Subject: [PATCH] fix: prevent sybil bypass via delete-then-re-claim and fix status code mismatch Bug 1 (Critical): delete-airdrop removes user record but does not track the verification token. A user can claim multiple times with different wallets using the same verified social account. Fixed by adding a DeletedToken model that tracks tokens from deleted claims, checked on re-verification. Bug 2 (Minor): Frontend checks HTTP 400 but backend returns HTTP 412 for traits-not-satisfied. Fixed frontend to match backend status code. Closes #24 --- pages/api/delete-airdrop.ts | 9 +++++++++ pages/api/verify-token.ts | 10 ++++++++++ pages/index.tsx | 4 ++-- prisma/schema.prisma | 8 ++++++++ 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/pages/api/delete-airdrop.ts b/pages/api/delete-airdrop.ts index 993ac79..3fad0b2 100644 --- a/pages/api/delete-airdrop.ts +++ b/pages/api/delete-airdrop.ts @@ -66,6 +66,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }); } + // Save the token to DeletedToken before deleting to prevent re-claim + if (existingUser.baseVerifyToken) { + await prisma.deletedToken.create({ + data: { token: existingUser.baseVerifyToken } + }).catch(() => { + // Token may already be in deleted_tokens (idempotent) + }); + } + // Delete the user from the database try { await prisma.verifiedUser.delete({ diff --git a/pages/api/verify-token.ts b/pages/api/verify-token.ts index 008b770..eacad0b 100644 --- a/pages/api/verify-token.ts +++ b/pages/api/verify-token.ts @@ -126,6 +126,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }); } + // Check if this token was previously used and deleted (sybil bypass prevention) + const previouslyDeleted = await prisma.deletedToken.findUnique({ + where: { token: baseVerifyToken } + }); + if (previouslyDeleted) { + return res.status(409).json({ + error: 'This verification token has been previously used and deleted. Cannot re-claim.' + }); + } + const result = await prisma.verifiedUser.upsert({ where: { address: walletAddress }, update: { diff --git a/pages/index.tsx b/pages/index.tsx index 7e0ea92..e0d8dd7 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -304,8 +304,8 @@ export default function Home({ initialUsers, error }: Props) { } else { const errorData = await response.json() - // Handle 400 with traits not satisfied - Twitter account not verified - if (response.status === 400 && errorData.message === 'verification_traits_not_satisfied') { + // Handle 412 with traits not satisfied - Twitter account not verified + if (response.status === 412 && errorData.error === 'X account does not satisfy verification requirements.') { setVerificationError('Sorry, your X account does not have a blue checkmark. You are not eligible for this airdrop.') setIsAutoVerification(false) // Reset flag } diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1bdc977..321e502 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -30,3 +30,11 @@ model CoinbaseVerifiedUser { @@map("coinbase_verified_users") } + +model DeletedToken { + id String @id @default(cuid()) + token String @unique + deletedAt DateTime @default(now()) @map("deleted_at") + + @@map("deleted_tokens") +}