트리거, 자동 규칙, 비즈니스 로직
수동 개입 없이 도는 DB 트리거, 비즈니스 규칙, 시스템 동작을 다룬다.
데이터베이스 트리거
안전 한계 없음: NAV는 자동으로 조정된다
PD6(NAV Safety Bounds)은 제거됐다. 거래당 한도도 없고 하한도 없다. 부실 이벤트가 발생하면 NAV가 자동으로 조정되고, 시장 가격이 실제 자산 가치를 반영한다. 유일한 제약은 상한 $1.00이다. 어느 NAV 수준에서도 투자는 열려 있고, 신규 투자자는 현재의 공정 가격을 낸다.
NAV 하락 타임락(24시간)
오라클이 NAV 하락을 올리면 nav_history에 effective_at = now() + 24h, 상태 PENDING으로 기록된다. 실제 pools.nav_per_token은 타임락이 지난 뒤에야 갱신된다. NAV 상승(회복)은 타임락을 건너뛰고 즉시 적용된다. 낮아진 가격이 적용되기 전에 이해관계자가 대응할 시간을 준다.
LP 전송 시 자동 정산
LP 토큰 전송(PLATFORM_ISSUED) 직전에 미청구 수익이 보내는 쪽에 자동으로 정산된다. 토큰이 움직일 때 수익을 잃지 않게 한다.
비즈니스 규칙
투자 사전 확인
입금을 허용하기 전에 kyc_status = APPROVED이고 lifecycle_status = ACTIVE이고 is_paused = false이고 tvl < capacity여야 한다. NAV 변동은 투자를 막지 않는다. 신규 투자자는 언제나 현재의 공정 가격을 낸다. 입금을 멈추는 것은 수동 조작인 is_paused이고, "Fully Subscribed"는 tvl >= capacity에서 파생된다(플래그가 없다). (investment_blocked는 삭제됐다. 마이그레이션 0018, 08의 v3-29.)
투자 건당 활성 상환 요청 1개
시스템이 유니크 제약을 강제한다. investment_id당 종료되지 않은(COMPLETED/REJECTED가 아닌) 상환 요청은 하나뿐이다. 투자자는 그것이 해소돼야 다시 요청할 수 있다.
상환 요청 시 NAV 스냅샷
nav_at_request는 요청이 생성될 때 잡히고 다시 계산되지 않는다. 지급액은 tokens_minted × nav_at_request + accrued_yield다. 처리 중에 NAV가 더 떨어져도 투자자를 보호한다. 반대로 요청 후 NAV가 회복돼도 그 이득을 보지 못한다는 뜻이기도 하다.
⚠️ 스냅샷은 예고된 NAV이지 항상 pools.nav_per_token인 것은 아니다. 하락이 24시간 타임락에 있는 동안 요청은 PoolCommonLib.effectiveNav(= pendingNav)로 가격이 매겨진다. 그래야 예고 기간 안의 이탈이 상각을 앞질러 빠져나가면서 남은 홀더에게 상각을 떠넘기지 못한다. reserve 게이트와 고정되는 스냅샷이 같은 값을 읽는다. 전체 규칙은 R2·R3와 07-redemption → 지급 산식 참조.
명목 잔액 기준 수익
발생 수익은 NAV로 조정된 토큰 가치가 아니라 언제나 명목(최초) 투자 금액 기준으로 계산한다. 산식은 nominal_amount × (APY / 365) × days_held다. NAV가 떨어져도 남아 있을 유인을 준다. 원래 입금액에 대한 수익을 온전히 받기 때문이다.
상환 대기열(즉시 풀은 FIFO)
즉시 풀(epoch_duration_days = 0)은 오래된 요청부터 처리한다. 어드민이 승인 전에 유동성을 확인하고, 유동성이 모자라는 요청은 거부되지 않고 대기열에 남는다(PENDING_RESERVE). 회차 풀(epoch_duration_days > 0, v3-26)은 FIFO를 쓰지 않고 회차 마감 시점에 pro-rata로 정산한다(요청 순서에 우선권이 없다). 상환 → 회차 기반 참조. ⚠️ v3-91 재설계가 결정됐다(구현 대기). 정산 시점이 cutoff + recallLeadDays로 옮겨 가고 이월이 이월분 우선 FIFO가 된다.
모든 풀은 fund가 운영한다(fund_id 필수, v3-26). FM 사전 확인 단계는 없다. FM_ACCEPTED는 v3-34에서 제거됐고, 파트너의 유일한 행동은 reserve 부족분을 채우는 것이다(워크플로 단계가 아니라 상태다). (v2.x의 AS Pool / FUND_POOL 분리와 에스크로 모델은 제거됐다. v3-01/v3-11.)
조기 상환 패널티
락업 기간(LOCKED) 중에는 상환이 완전히 차단된다(v3-76). 락업이 끝나고 만기 전인 구간(EARLY)에는 풀별로 설정한 패널티가 적용된다(풀 생성 시 설정한다). 타입 정의는 04-pool-models § 패널티 타입, 상환 시점 계산은 07-redemption § 패널티 타입 참조.
풀 lifecycle 자동 전이(스케줄러)
매일 도는 스케줄러가 확인한다. (1) start_date ≤ 오늘인 UPCOMING 풀은 ACTIVE로 자동 전이한다. (2) end_date ≤ 오늘인 ACTIVE 풀은 온체인을 먼저 처리한 뒤 MATURED로 자동 전이하고, 홀더에게 pool_matured 알림을 보낸다. 자동 상환은 없다(S-21). 상환은 투자자가 온체인에서 직접 시작하며(requestRedemption) 어드민이나 키퍼가 홀더를 대신해 상환하는 경로가 없다. 만기는 상환에서 패널티만 없앤다(MATURED = FREE). DRAFT → UPCOMING은 어드민이 "Publish"를 명시적으로 눌러야 한다.
Fully Subscribed(TVL 상한)
풀의 tvl >= capacity가 되면 "Fully Subscribed"가 되고 신규 입금이 거부된다. 상환 등으로 TVL이 capacity 아래로 내려가면 자동으로 다시 열린다. 이 판단은 입금 시점의 tvl과 capacity에서 파생되고 저장된 플래그가 없다. (옛 investment_blocked 플래그는 삭제됐다. 마이그레이션 0018, 08의 v3-29.)
SBT 민팅 실패와 재시도(큐 기반)
KYC가 승인되면(kyc_status = 'APPROVED') 시스템이 SBT 민팅을 FIFO sbt-mint 큐에 적재한다(메시지 그룹이 하나라 단일 서명 지갑에서 직렬 처리된다). 컨슈머가 Base L2에서 민팅하고, 트랜잭션이 revert되거나 타임아웃되면 5회 재시도한 뒤 **DLQ(14일 보존)**로 들어가 CloudWatch 알람에서 ops 이메일이 나간다. 결과는 sbt_status에 반영된다(MINTED / FAILED, sbt_error 기록).
복구는 두 겹이다. 큐 재구동이 일시적 트랜잭션 실패를 처리하고, reconcile sweep(5분 주기, 10분 정체 기준)이 여전히 APPROVED + NOT_MINTED/FAILED인 것을 다시 큐에 넣으며 SumSub webhook 유실(IN_REVIEW)도 보정한다. 어드민 수동 재시도(POST /kyc/mint-sbt)도 큐에 적재하는 방식이라 tx 해시가 아니라 202 { status: 'queued' }를 반환한다. reject_type = 'FINAL'이면 이후 모든 재시도가 막힌다. 소각(최초 거부)과 취소(승인 후 AML 적발)도 nonce 안전을 위해 같은 큐를 탄다.
알림 재시도(3회 지수 백오프)
이메일 발송은 시도 단위로 **notification_deliveries**에 기록한다(마이그레이션 0110에서 notification_logs를 삭제했다). 재시도는 SQS 재전달이고 1/5/15/60/240분 백오프를 거친 뒤 DLQ로 간다. 행의 status는 SENT, FAILED, BOUNCED, COMPLAINED, SKIPPED 중 하나로 끝난다. 대시보드 알림 드롭다운에 뜨는 것은 FAILED뿐이다. SKIPPED는 의도적으로 보내지 않은 것이고(수신 거부, 주소 없음, 차단됨), INVALID_RECIPIENT는 KPI에서 제외된다(0107). 실패 유형 중 BOUNCED, SPAM_FILTERED, SERVER_ERROR, INVALID_RECIPIENT는 실제로 쓰이고, TIMEOUT과 RATE_LIMITED는 쓰이지 않는다. → 18-failure-types · 13-operations
풀 노출 규칙
대시보드(공개)
lifecycle_status IN ('UPCOMING', 'ACTIVE', 'CLOSED', 'MATURED')이고 is_paused = false인 풀만 보여 준다. DRAFT 풀은 투자자에게 절대 보이지 않는다. NAV가 par 아래여도 풀을 숨기지 않는다. 투자는 열려 있고 가격이 공정 가치를 반영한다. 예외로, 유저가 포지션을 보유하고 있으면 상태와 무관하게 그 풀은 항상 보인다.
포트폴리오(투자자)
풀 상태와 무관하게 유저의 모든 투자를 보여 준다. 일시 중단, 상각, 심지어 오프보딩된 풀도 적절한 상태 배지와 함께 표시된다.
어드민 패널
Admin과 Operator는 모든 풀을 본다(일시 중단된 것 포함). 펀드매니저는 배정된 fund의 풀만 본다. 오라클 상태는 Admin과 Operator가 /admin/oracle에서 볼 수 있다.