Skip to content

Failure Types Reference

All failure states, error types, and recovery actions across Aset entities. Each failure type has corresponding mock data in apps/infra/db/seed.sql.

⚠️ Illustrative — not the code-emitted set

This page was authored as a seed/mock reference, so most failure_type values and error strings in the tables below are illustrative examples, not values the backend actually writes. Do not build FE failure_type switches against them.

Actually emitted by code (V1):

  • Depositsno failure_type writer exists. A failed deposit only sets status = FAILED (no typed reason).
  • RedemptionsINVESTOR_CANCELLED (investor self-cancel; persisted with status = REJECTED) and an admin-supplied reason on reject (else REJECTED). There is no dedicated payout-failure failure_type.
  • YieldON_CHAIN_TX_FAILED (on-chain distribute tx failure — yield-distributions.post.create.ts, lib/shared/yield/run-distribution.ts).
  • NotificationsINVALID_RECIPIENT and SERVER_ERROR from the deliver worker (notifications.worker.deliver.ts), plus BOUNCED and SPAM_FILTERED from the live SES event path (notifications.webhook.ses.ts). Only TIMEOUT and RATE_LIMITED remain unproduced — nothing writes them today.

Treat the tables below as display-copy guidance for seeded/mock rows, not the runtime taxonomy.

Overview

CategoryCount
Deposit States5
Redemption Failures5
Yield Failures4
KYC/SBT States3
Notification Failures6
Deposit Failures deposits.status + failure_type

Handle deposit processing, escrow management, and LP token minting with recovery workflows.

Status flow: PENDINGPROCESSINGCOMPLETED or FAILED

StatusFailure TypeError ExampleAdmin DisplayInvestor DisplayRecovery
FAILEDLP_MINT_FAILEDERC20: transfer amount exceeds balanceRed "LP Mint Failed" + error"Your deposit could not be processed"Admin retries
FAILEDFM_NOTIFICATION_FAILEDSMTP delivery failed: mailbox unavailableYellow "FM Not Notified" + retryNot shown (internal)Admin retries FM notification
PROCESSINGBlue "Processing" + spinner"Your deposit is being processed"Auto-escalate if stuck > 1hr
Redemption Failures redemption_requests.status + failure_type

Workflow with lockup validation and reserve/funding checks. No FM pre-acknowledge step (FM_ACCEPTED removed in v3-34).

Status flow (instant): REQUESTEDPROCESSINGCOMPLETED (or PENDING_RESERVE while awaiting partner funds) Status flow (epoch, v3-26): REQUESTEDQUEUEDPARTIALLY_FILLED* → COMPLETED (*partial fills roll over until claimed)

StatusFailure TypeError ExampleAdmin DisplayInvestor DisplayRecovery
FAILEDPAYOUT_TX_FAILEDInsufficient reserve balance in escrowRed "Payout Failed" + escrow info"Redemption could not be completed"Admin tops up escrow, retries
FAILEDFM_NOTIFICATION_FAILEDSMTP connection refused on port 587Yellow "FM Not Notified"Not shown to investorRetry FM notification
FAILEDCONTRACT_EXECUTION_FAILEDexecution reverted: InsufficientReserveRed "Contract Error" + shortfall"Processing error — contact support"Admin addresses reserve shortfall
FAILEDFUND_TRANSFER_FAILEDERC20: transfer exceeds allowance — approval expiredRed "Transfer Failed" + deadline"Fund transfer pending"FM renews wallet approval
REJECTEDLOCKUP_NOT_METLockup period (180 days) has not elapsedGray "Rejected" + reason"Cannot redeem: lockup active"Wait until lockup expires

Mock data: Redemption IDs 7–11 in seed.sql.

Yield Distribution Failures yield_distributions.status + failure_type

Automated yield accrual, batch claim processing, and transaction failure handling.

Status flow: PROCESSINGDISTRIBUTED or FAILED

PENDING is gone — the orphan was fixed by deleting its producer (v3-104, migration 0113)

PENDING only ever existed for the sub-second gap between the legacy server-key create path's insert and settleYield (the FM path inserts PROCESSING and stops for POST /{id}/distribute; the indexer only ever writes DISTRIBUTED). A row that stayed there was a crash orphan nothing could heal — the indexer reconciles on tx_hash, which such a row had not been given yet, and yield.scheduler.reconcile only mirrors claimable_yield — so the period read as un-distributed forever with no way to tell from the DB whether holders were paid.

The planned fix was a sweeper. It was superseded: that legacy path was unreachable in product anyway (the admin UI refuses the server key for any fund_wallet pool, fund_wallet is required at create per v3-26, and a display-only pool has no pool_address to distribute from), so the path was deleted instead — POST /yield-distributions 400s without deposit_tx_hash — and migration 0113 made the status unrepresentable: default PROCESSING + CHECK (status <> 'PENDING'). A pre-existing PENDING row with no tx was backfilled to FAILED / ORPHANED_LEGACY_PATH; one carrying a tx would have aborted the migration for manual on-chain reconciliation, since marking a possibly-settled period FAILED records a paid period as unpaid.

⚠️ The yield_status enum keeps PENDINGyield_distribution_investors.status uses it legitimately (an allocation not yet claimed). The ban is a CHECK on yield_distributions only.

A PROCESSING row older than 24h is the state that needs a human

Not a failure type — a stall. The FM's depositYield landed on-chain (the row carries its deposit_tx_hash), so the yield is in the pool, but POST /{id}/distribute was never run and holders have not been paid. Recovery exists: that endpoint, plus the Distribute action on the admin Yield table (offered exactly when there is a deposit tx to verify; disabled with the reason when there is not — a row with no deposit_tx_hash came from the removed server path and is reconciled by the indexer, so retrying it risks paying twice).

Surfaced as dashboard_alert_counts.stalled_yield → the dashboard's "Stalled Distributions" alert, and under the Yield screen's Failed tab as Stalled. The matching ADMIN notification is not built — blocked on copy (v3-104).

StatusFailure TypeError ExampleAdmin DisplayInvestor DisplayRecovery
FAILEDORPHANED_LEGACY_PATHRecorded by the removed legacy server-key create path and never settled (no deposit or settle tx)Red "Orphaned Legacy Path"Not shownRecord the period again (nothing reached the chain)
FAILEDTX_FAILEDGas price exceeded maximum thresholdRed "TX Failed" + gas info"Yield distribution delayed"Admin retries with higher gas
FAILEDFM_NOTIFICATION_FAILEDSendGrid API error 429: rate limit exceededYellow "FM Not Notified"Not shownRetry after rate limit resets
FAILEDDISTRIBUTION_TX_FAILEDERC20 transfer failed — pool escrow insufficientRed "Distribution Failed" + tx hash"Yield distribution delayed"Admin tops up escrow
FAILEDCLAIM_PROCESSING_FAILEDBatch claim tx gas estimation failed: out of gas for 4 recipientsRed "Claim Failed" + recipient count"Yield claim pending"Admin splits batch, retries

Notes

Batch optimization for gas limits. Auto-retry up to 3x with exponential backoff. SendGrid rate limit: delayed retry (30min–2hr). Mock data: Yield IDs 8–11 in seed.sql.

KYC / SBT Failures users.kyc_status + sbt_status

Identity verification and Soul Bound Token minting with async mint operations.

KYC flow: PENDINGAPPROVED / REJECTED

SBT flow: NOT_MINTEDMINTED / FAILED

EntityStatusDetailAdmin DisplayInvestor DisplayRecovery
KYCREJECTEDFailed identity verificationRed "KYC Rejected" + reason"Verification unsuccessful — resubmit"User resubmits KYC
KYCPENDINGAwaiting reviewYellow "Pending Review""Under review"Admin reviews
SBTFAILEDTX timeout or chain errorRed "SBT Mint Failed" + tx hash"Credential issuance failed" + retryAdmin triggers re-mint
Notification Failures notification_deliveries.status + .failure_type

⚠️ notification_logs no longer exists. Migration 0110 split it into three tables (v3-103); a delivery outcome now lives on notification_deliveriesstatus (the delivery_status enum) plus an optional failure_type. See 11-db-schema → Notifications and 13-operations → Data model.

Status flow: PENDINGSENT → (SES event) BOUNCED / COMPLAINED, or FAILED on a send error, or SKIPPED when we deliberately did not send.

SKIPPED is the one that is not a failure and must not be styled as one — it records "we chose not to email this person, and why" (OPTED_OUT / NO_ADDRESS / SUPPRESSED_ADDRESS). Only FAILED counts toward the failed-notification KPI; INVALID_RECIPIENT is excluded from it too (0107).

StatusLive?Written byMeaningAuto-retryRecovery
SENTnotifications.worker.deliver.tsAccepted by SES
FAILEDnotifications.worker.deliver.tsSend threw (SERVER_ERROR, INVALID_RECIPIENT)SQS 1/5/15/60/240m → DLQFix config / recipient address
BOUNCEDnotifications.webhook.ses.tsSES bounce; a hard bounce also suppresses the address (HARD_BOUNCE)NoUpdate the address, then un-suppress
COMPLAINEDnotifications.webhook.ses.tsRecipient marked it spam; suppresses the address (COMPLAINT)NoDo not re-add without consent
SKIPPEDpreferences/decide.tsDeliberate non-send, reason recordedNone needed
PENDINGon fan-outQueued, not yet attempted

failure_type values that are still unproduced. TIMEOUT and RATE_LIMITED exist in notification_failure_type but nothing writes them. SPAM_FILTERED is written now — the SES complaint path maps to it. So of the original six, four are live and two remain reserved.

Mock data: SBT mint failure, LP mint failure, FM bounce in seed.sql.