Summary
Two bugs found in the sybil protection and verification flow:
Bug 1 (Critical): Delete-then-re-claim bypasses anti-sybil protection
The delete-airdrop endpoint removes a user's record from the database, but does not track the verification token afterwards. This allows a user to claim the airdrop multiple times with different wallets using the same verified social account.
Steps to reproduce:
- Connect Wallet A → verify X account → get token abc123 → claim airdrop ✅
- Call POST /api/delete-airdrop with Wallet A's signature → record deleted ✅
- Connect Wallet B → verify the same X account → get the same token abc123
- POST /api/verify-token → findUnique({ where: { baseVerifyToken: "abc123" } }) returns null (old record was deleted)
- Upsert creates a new record with Wallet B's address and token abc123 ✅
- User claims the airdrop again with a different wallet 🎉
Affected file: pages/api/verify-token.ts (lines 83-103) — the token uniqueness check only looks at existing records, not deleted ones.
Affected file: pages/api/delete-airdrop.ts (whole file) — deletes without tracking the token.
Impact: The core anti-sybil mechanism described in the README ("Same provider account always produces same token → Database rejects duplicate tokens → prevents multi-wallet abuse") is completely bypassable. A user can claim the airdrop N times with N different wallets using the same social account.
Suggested fix: Add a DeletedToken table (or a deleted_tokens set) that tracks tokens from deleted claims. On re-verification, check both the existing records AND the deleted-token list before allowing a new claim.
Bug 2 (Minor): Frontend checks for HTTP 400, backend returns HTTP 412
In pages/index.tsx, the frontend checks for response.status === 400 to show a "traits not satisfied" error message:
typescript
// pages/index.tsx (frontend)
if (response.status === 400 && errorData.message === 'verification_traits_not_satisfied') {
setVerificationError('Sorry, your X account does not have a blue checkmark...')
}
However, in pages/api/verify-token.ts, when the Base Verify API returns a 400 with verification_traits_not_satisfied, the backend maps it to HTTP 412 (Precondition Failed):
typescript
// pages/api/verify-token.ts (backend)
if (verifyResponse.status === 400) {
const errorData = JSON.parse(responseBody);
if (errorData.message === 'verification_traits_not_satisfied') {
return res.status(412).json({ // <-- 412, not 400
error: 'X account does not satisfy verification requirements.'
});
}
}
Impact: The error handling branch for "traits not satisfied" is dead code on the frontend. Users who don't meet trait requirements see a generic error message instead of the helpful "no blue checkmark" message.
Suggested fix: Change the frontend check from response.status === 400 to response.status === 412, or change the backend to return 400 instead of 412.
Summary
Two bugs found in the sybil protection and verification flow:
Bug 1 (Critical): Delete-then-re-claim bypasses anti-sybil protection
The delete-airdrop endpoint removes a user's record from the database, but does not track the verification token afterwards. This allows a user to claim the airdrop multiple times with different wallets using the same verified social account.
Steps to reproduce:
Affected file: pages/api/verify-token.ts (lines 83-103) — the token uniqueness check only looks at existing records, not deleted ones.
Affected file: pages/api/delete-airdrop.ts (whole file) — deletes without tracking the token.
Impact: The core anti-sybil mechanism described in the README ("Same provider account always produces same token → Database rejects duplicate tokens → prevents multi-wallet abuse") is completely bypassable. A user can claim the airdrop N times with N different wallets using the same social account.
Suggested fix: Add a DeletedToken table (or a deleted_tokens set) that tracks tokens from deleted claims. On re-verification, check both the existing records AND the deleted-token list before allowing a new claim.
Bug 2 (Minor): Frontend checks for HTTP 400, backend returns HTTP 412
In pages/index.tsx, the frontend checks for response.status === 400 to show a "traits not satisfied" error message:
typescript
// pages/index.tsx (frontend)
if (response.status === 400 && errorData.message === 'verification_traits_not_satisfied') {
setVerificationError('Sorry, your X account does not have a blue checkmark...')
}
However, in pages/api/verify-token.ts, when the Base Verify API returns a 400 with verification_traits_not_satisfied, the backend maps it to HTTP 412 (Precondition Failed):
typescript
// pages/api/verify-token.ts (backend)
if (verifyResponse.status === 400) {
const errorData = JSON.parse(responseBody);
if (errorData.message === 'verification_traits_not_satisfied') {
return res.status(412).json({ // <-- 412, not 400
error: 'X account does not satisfy verification requirements.'
});
}
}
Impact: The error handling branch for "traits not satisfied" is dead code on the frontend. Users who don't meet trait requirements see a generic error message instead of the helpful "no blue checkmark" message.
Suggested fix: Change the frontend check from response.status === 400 to response.status === 412, or change the backend to return 400 instead of 412.