Skip to content

Triggers, Auto-Rules & Business Logic

Automated database triggers, business rules, and system behaviors that run without manual intervention.

Database Triggers

🔓 No Safety Bounds — NAV Auto-Adjusts

PD6 (NAV Safety Bounds) removed. No per-transaction limit, no floor. NAV auto-adjusts on default events — the market price reflects the actual asset value. Only constraint: max $1.00 cap. Investment stays open at all NAV levels — new investors pay fair current price.

🔒 NAV Decrease Timelock (24 Hours)

When Oracle posts a NAV decrease, it's logged in nav_history with effective_at = now() + 24h and status PENDING. The actual pools.nav_per_token only updates after the timelock expires. NAV increases (recovery) bypass the timelock and apply immediately. Gives stakeholders time to react before a lower price takes effect.

🔗 LP Transfer Auto-Settlement

Before any LP token transfer (PLATFORM_ISSUED), unclaimed yield is auto-settled to the sender. Prevents yield loss on token movement.

Business Rules

🚫 Investment Pre-Checks

Before allowing deposit: kyc_status = APPROVED AND lifecycle_status = ACTIVE AND is_paused = false AND tvl < capacity. NAV changes do not block investment — new investors always pay the current fair price. The deposit halt is is_paused (manual); "Fully Subscribed" derives from tvl >= capacity (no flag). (investment_blocked dropped — migration 0018, 08 v3-29.)

1️⃣ One Active Redemption Per Investment

System enforces unique constraint: only one non-terminal (not COMPLETED/REJECTED) redemption request per investment_id. Investor must wait for resolution before requesting again.

📸 NAV Snapshot on Redemption Request

nav_at_request is captured at request creation and never recalculated. Payout = tokens_minted × nav_at_request + accrued_yield. Protects investor from further NAV drops during processing. Also means investor doesn't benefit from NAV recovery after request.

⚠️ The snapshot is the announced NAV, not always pools.nav_per_token. While a decrease sits in its 24h timelock, the request is priced at PoolCommonLib.effectiveNav (= pendingNav), so an exit inside the notice window cannot outrun the write-down and leave it with the holders who stayed. Both the reserve gate and the pinned snapshot read the same value. Full rule: R2·R3 · 07-redemption → Payout formula.

📐 Yield on Nominal Balance

Accrued yield always calculated on nominal (original) investment amount, not NAV-adjusted token value. Formula: nominal_amount × (APY / 365) × days_held. This incentivizes investors to stay through NAV dips — they still earn full yield on their original deposit.

📋 Redemption Queue (FIFO for instant pools)

Instant pools (epoch_duration_days = 0) process requests oldest-first; admin checks liquidity before approving, and insufficient-liquidity requests stay in queue (PENDING_RESERVE, not rejected). Epoch pools (epoch_duration_days > 0, v3-26) ignore FIFO and settle pro-rata at the epoch deadline (no age priority) — see Redemption → Epoch-Based. ⚠️ v3-91 redesign decided (impl pending): settlement moves to cutoff + recallLeadDays and rollover becomes carry-first FIFO.

All pools are fund-operated (fund_id required, v3-26). There is no FM pre-acknowledge stepFM_ACCEPTED was removed in v3-34; the partner's only action is funding a reserve shortfall (a state, not a workflow step). (v2.x AS Pool / FUND_POOL split + escrow models removed — v3-01/v3-11.)

⚖️ Early Redemption Penalty

During the lockup period (LOCKED) redemption is blocked entirely (v3-76). Once lockup ends but before maturity (the EARLY window), a per-pool configurable penalty applies (configured at pool creation). Type definitions → 04-pool-models § Penalty Types; redemption-time calculation → 07-redemption § Penalty Types.

📅 Pool Lifecycle Auto-Transitions (Scheduler)

Daily scheduler checks: (1) UPCOMING pools where start_date ≤ today → auto-transition to ACTIVE. (2) ACTIVE pools where end_date ≤ today → auto-transition to MATURED (on-chain-first), then a pool_matured notification to holders. No auto-redemption (S-21): redemption is investor-initiated on-chain (requestRedemption) — there is no admin/keeper path to redeem on a holder's behalf; maturity only makes redemption penalty-free (MATURED = FREE). DRAFT → UPCOMING requires explicit admin "Publish" action.

🚫 Fully Subscribed (TVL Cap)

When pool tvl >= capacity, the pool is "Fully Subscribed" and new deposits are rejected; it reopens automatically once TVL drops below capacity (e.g., after redemptions). This is derived from tvl/capacity at deposit time — there is no stored flag. (The former investment_blocked flag was dropped — migration 0018, 08 v3-29.)

🪨 SBT Mint Failure & Retry (queue-based)

On KYC approval (kyc_status = 'APPROVED'), the system enqueues the SBT mint onto the FIFO sbt-mint queue (single message group → serialized on the one signer wallet). The consumer mints on Base L2; if the tx reverts or times out it is retried 5×, then lands in the DLQ (14-day retention) which fires a CloudWatch alarm → ops email. sbt_status mirrors the outcome (MINTED / FAILED, sbt_error logged).

Recovery is two-layered: the queue redrive handles transient tx failures, and the reconcile sweep (every 5 min, 10-min stuck threshold) re-queues anything still APPROVED + NOT_MINTED/FAILED and heals SumSub webhook losses (IN_REVIEW). Admin manual retry (POST /kyc/mint-sbt) also enqueues — it returns 202 { status: 'queued' }, not a tx hash. reject_type = 'FINAL' blocks all future retry attempts. Burn (initial rejection) and revoke (post-approval AML hit) flow through the same queue for the same nonce-safety reason.

📨 Notification Retry (3x Exponential Backoff)

Email delivery is tracked per attempt on notification_deliveries (migration 0110 dropped notification_logs). Retries are SQS redeliveries — 1/5/15/60/240-minute backoff, then the DLQ — and the row's status lands on SENT, FAILED, BOUNCED, COMPLAINED or SKIPPED. Only FAILED reaches the Dashboard alert dropdown: SKIPPED is a deliberate non-send (opted out / no address / suppressed) and INVALID_RECIPIENT is excluded from the KPI (0107). Failure types: BOUNCED, SPAM_FILTERED, SERVER_ERROR, INVALID_RECIPIENT are live; TIMEOUT and RATE_LIMITED are unproduced. → 18-failure-types · 13-operations

Pool Visibility Rules

🏠 Dashboard (Public)

Shows only pools where lifecycle_status IN ('UPCOMING', 'ACTIVE', 'CLOSED', 'MATURED') AND is_paused = false. DRAFT pools are never visible to investors. NAV below par does not hide a pool — investment stays open and price reflects fair value. Exception: user has an active position → pool always visible regardless of status.

📂 Portfolio (Investor)

Shows ALL user investments regardless of pool status. Paused, writedown, even offboarded pools still display with appropriate status badges.

⚙️ Admin Panel

Admin/Operator see all pools (including paused). Fund Manager only sees pools under their assigned fund(s). Oracle health status visible to Admin + Operator via /admin/oracle.