풀 모델
v3.0 구현 완료(테스트넷)
v3.0 풀 모델은 컨트랙트, DB, 백엔드에 모두 구현됐고 Sepolia 테스트넷에 배포됐다(2026-06-04, Clones/EIP-1167 팩토리, 컨트랙트 테스트 183건 통과). 메인넷 정식 출시 전에 남은 것은 외부 보안 감사, 신규 제어 기능(wind-down, 거버넌스 타임락, 긴급 동결)에 대한 어드민·투자자 UI, 프로덕션 배포다.
Aset 풀이 어떻게 구성되는지를 다룬다. 각 풀은 고정된 타입이 아니라 독립적인 설정값들의 조합으로 정의된다. 덕분에 코드를 바꾸지 않고 설정만 바꿔서 새 파트너(Joob, LINE BK, 앞으로 들어올 오리지네이터)를 추가할 수 있다.
개요
v3.0 이전에는 풀 타입이 AS_POOL과 FUND_POOL 둘뿐이었다. 요구사항이 다른 파트너가 늘어나면서 설정 기반 구성으로 바꿨다. 각 풀은 커스터디, 자산 정보, 수익, 투자자 조건, 컴플라이언스, 런타임 상태를 아우르는 약 24개의 독립 설정을 갖는다.
이 페이지에서 다루는 것:
- 풀을 정의하는 설정(차원)
- 어떤 풀에서든 자금이 흐르는 방식
- 투자자에게 보여 주는 리스크 안내
- 예시 풀(현재 운영 중인 것과 계획 중인 것)
관련 페이지
- 스마트 컨트랙트: 컨트랙트 구조와 역할
- 투자 라이프사이클: 입금·상환·수익 플로우
- Writedown & NAV: NAV 관리
- 상환: 상환 플로우 상세
- DB 스키마: 풀 테이블 컬럼
핵심 원칙
1. 비수탁
Aset은 투자자 자금을 보관하지 않는다. 모든 돈은 스마트 컨트랙트를 통해 흐른다. Aset은 상환 승인 같은 특정 함수를 호출할 수 있지만 혼자서 자금을 옮길 수는 없다. 자금을 움직이는 것은 컨트랙트 코드뿐이다. VASP 라이선스가 필요 없다.
비수탁 6기준, 키 모델, money-path 불변식, 업그레이드 거버넌스를 한데 모은 정본은 **09a-custody**다.
| 단계 | 위치 | 보관 주체 |
|---|---|---|
| 입금 수령(즉시) | Pool 컨트랙트 | 컨트랙트 코드 |
| LP 토큰(입금 시 민팅) | 투자자 지갑 | 투자자 |
Reserve (reserve_bps) | reserveWallet | 외부 지갑 서명자 |
| 외부 펀드 자본(파트너 잔여분, 릴리스 후) | fund_wallet(파트너가 통제. Aset 트레저리가 아니다, v3-26) | 외부 주체 |
수익(depositYield()로 수령) | Pool 컨트랙트 | 컨트랙트 코드 |
| 수익(분배 완료, claim 전) | Pool 컨트랙트 | 컨트랙트 코드 |
| 수익(claim 후) | 투자자 지갑 | 투자자 |
| 수수료(net 계산 후) | Aset 트레저리 지갑 | Aset(멀티시그) |
| 상환 지급 | Pool 컨트랙트 → 투자자 | 컨트랙트 코드 |
| Wind-down 배분(풀 보유 유동성) | Pool 컨트랙트 → 투자자 | 컨트랙트 코드 |
표시 전용 풀
custody_mode = 'MIRROR'인 풀(예: 외부 컨트랙트에서 추적하는 레거시 투자자 포지션)에는 위 내용이 하나도 적용되지 않는다. Aset은 파트너가 보고한 것을 미러링할 뿐이다. 우리 쪽에는 자금도 커스터디도 LP 발행도 없다. ⚠️ 0203 이전에는 is_display_only였다. 그 컬럼은 이제 is_showcase이고 v3-40 마케팅 등급만을 뜻한다.
2. LP는 항상 Aset이 민팅한다
투자자가 Aset을 통해 입금하면, 기초 펀드를 어느 파트너가 운영하든 Aset의 PlatformLPToken 컨트랙트가 LP 토큰을 민팅한다.
그래서 이렇게 된다:
- LP 토큰 컨트랙트 하나가 모든 투자자 포지션을 한곳에서 추적한다
- 파트너가 자기 토큰 컨트랙트를 배포할 필요가 없다
- 설정 변경만으로 새 파트너를 붙일 수 있다
- 파트너 webhook은 없다. 입금에 대한 FM의 기록이 곧 플랫폼이다.
GET /deposits가 fund 범위로 동작하고deposit_confirmed_ops가fundManagers를 담고 있으므로, 동기화해야 할 파트너 쪽 사본이 존재하지 않는다(v3-115)
LP 발행을 중앙화한 이유는 v3-02에 있다(v2.x의 FUND_ISSUED는 파트너가 직접 민팅하게 했고, 그 기록이 Aset 기록과 어긋났다).
3. Reserve와 Tranche는 다르다
계속 혼동되는 둘인데, 손실을 처리하는 것은 그중 하나뿐이다(R8).
Reserve(reserve_bps, 기본 1000 = 10%): 유동성이지 손실 흡수가 아니다
- 용도: 상환과 타이밍 공백을 메우는 현금 버퍼
- 출처: 각 입금의 10%가 Pool 컨트랙트에 남는다
- 예시: 투자자가 상환을 원하는데 파트너가 자금을 보내는 데 며칠 걸리는 경우, reserve가 즉시 지급을 감당한다
- 손실 계층이 아니다(R8): 투자자 입금에서 떼어낸 것이라 이미 claim NAV 가격 안에 들어 있다. 손실에서 상계하면 같은 돈을 두 번 세는 것이다. 단독 풀의 first-loss는 매니저 equity 버퍼(R6)다
- 풀별로 조정 가능하다
Tranche Group(tranche_group_id + tranche_role, v3-14)
- 용도: 연결된 여러 풀에 걸친 first-loss 자본 배분
- 동작 방식: 여러 SINGLE 풀이
tranche_group_id를 공유한다. 각 풀은tranche_role(SENIOR/MEZZANINE/JUNIOR)을 갖는다. NAV 계산 시 Aset 오라클 Lambda가 워터폴을 적용해 Junior가 먼저 손실을 흡수하고, 다음이 Mezzanine, 그다음이 Senior다. - 예시: LINE BK 상품은 Senior 풀(낮은 APY, 보호됨)과 Junior 풀(높은 APY, first-loss)로 구성된다. Junior 자본은 LFC가 댄다.
- 참여 풀에
tranche_group_id와tranche_role을 설정하면 활성화된다
풀은 다음 중 하나다:
- reserve만 가진 단독 풀(대부분의 풀.
tranche_group_id가 NULL이다) - reserve와 역할 기반 손실 워터폴을 함께 가진 트랜치 그룹의 일원
풀은 어떻게 정의되나
풀은 약 24개의 독립 설정(차원이라 부른다)으로 정의된다. 각 설정은 풀 동작의 한 측면을 통제한다. 풀은 이 차원들에 값을 조합해 구성하며, 고정된 풀 타입은 없다.
아래 색인과 모든 분류 표에서 쓰는 범례:
- 변경 가능성: 🔒 배포 후 불변 · 🟡 타임락 + 멀티시그로 변경 · 🟢 어드민이 즉시 변경
- 상태: ✅ 코드 구현됨 · 🔨 작성됐으나 라이브 아님(미적용 마이그레이션 또는 미배포 API) · 🚧 개발 중 · 📋 스펙만 있음(코드 없음)
색인: 모든 차원을 한 화면에
각 이름은 아래 전체 정의로 연결된다. 분류는 절 순서와 같다.
| 차원 | 통제 대상 | 변경 가능성 | 상태 |
|---|---|---|---|
| 자금 흐름과 커스터디: 돈이 어디로 가는가 | |||
fund_wallet | 각 입금의 파트너 잔여분을 보낼 곳 | 🟡 타임락 | 📋 |
reserve_bps | 각 입금 중 상환 유동성으로 풀에 남길 비율 | 🟡 타임락 | ✅ |
is_showcase | v3-40 마케팅 등급. 보이지만 여기서는 절대 투자 불가 | 🔒 | 📋 |
custody_mode | Aset이 이 풀의 자본을 보관·처리하는지 여부(PLATFORM | MIRROR) | 🔒 | ✅ |
is_hidden | 어드민 목록에서 풀을 숨긴다. 기능 변화는 없다 | 🟢 모든 상태 | ✅ |
| 자산 설정: 풀이 무엇에 투자하는가 | |||
chain_id | 풀이 배포되는 체인 | 🔒 | ✅ |
accepted_currencies | 풀이 받는 USD 기반 스테이블코인 | 🔒 첫 입금 이후 | ✅ |
operating_currency | 기초자산의 표시 통화(백엔드 전용) | 🔒 | ✅ |
fx_rate_source | 그 통화에 대해 고정 환율을 쓸지 파트너 피드를 쓸지 | 🟢 DRAFT에서만 | ✅ |
collateral_description | 자산을 무엇이 담보하는지 서술한 자유 텍스트 | 🟢 즉시 | 📋 |
collateral_ratio | 노출 대비 담보 비율 | 🟢 즉시 | 📋 |
| 수익 설정: 수익이 투자자에게 도달하는 방식 | |||
yield_frequency | 분배가 얼마나 자주 있을 것으로 보는가 | 🟢 즉시 | ✅ |
net_yield_fee_config | 투자자에게 크레딧되기 전에 적용되는 수수료 구성 | 🟢 즉시 | 📋 |
| 투자자 조건: 락업, 만기, 패널티, 트랜치 | |||
lockup_days | 포지션을 아예 상환할 수 없는 기간 | 🟢 DRAFT에서만 | ✅ |
maturity_model | FIXED_TERM(만기 있음)과 OPEN_ENDED | 🔒 | 📋 |
maturity_days | 풀의 만기 시점 | 🟢 DRAFT에서만 | ✅ |
redemption_type | 만기 전 이탈을 허용하는지 여부 | 🔴 생성 시에만 | ✅ |
penalty_type | 조기 이탈의 비용과 그 산정 기준 | 🟢 DRAFT에서만 | ✅ |
tranche_group_id | 풀이 속한 트랜치 그룹 | 🔒 | 📋 |
tranche_role | 그 그룹 안에서의 SENIOR / MEZZANINE / JUNIOR | 🔒 | 📋 |
allow_rollover | ⚠️ MVP에서는 어느 화면에도 노출하지 않는다(2026-08-27, v3-151). 발생 수익을 청구 대신 재투자할 수 있는지 여부 | 🟢 즉시 | ✅ |
| 컴플라이언스: 누가 들어올 수 있는가 | |||
enforce_jurisdiction | 관할 게이트를 켠다 | 🟡 타임락 | ✅ |
jurisdiction_whitelist | 게이트가 켜졌을 때 허용되는 ISO 코드 | 🟡 타임락 | ✅ |
allows_us_persons | 미국인 투자 허용 여부 | 🟡 타임락 | ✅ |
| 풀 상태: 배포 시 설정하는 값이 아니라 런타임 값 | |||
lifecycle_status | DRAFT → ACTIVE → … → WIND_DOWN | 자동 + 어드민 트리거 | ✅ |
is_paused | soft pause. 신규 입금을 막고 출구는 열어 둔다 | 🟢 즉시 | ✅ |
is_emergency_frozen | hard freeze. 기간이 정해져 있고 스스로 만료된다 | 🟢 즉시 | ✅ |
| 운영 / 고급: 선택적 제어 | |||
epoch_duration_days | 0이면 즉시 상환, > 0이면 회차 묶음 처리 | 🔴 생성 시에만 | ✅ |
redemption_gating_bps | ⚠️ 폐기됨. MVP에 포함되지 않는다(2026-08-27, v3-150). 한 회차에 정산 가능한 예치 자본 상한 | 🟡 타임락 | 📋 |
redemption_term_epochs | 만기 후 몇 회차에 걸쳐 상환하는지(및 epoch_date_basis · epoch_roll_day) | 🔴 생성 시에만 | 🔨 |
nav_deviation_cap_bps | 이 폭을 넘는 NAV 갱신을 거부한다 | 🟡 타임락 | ✅ |
nav_staleness_seconds | 이보다 오래된 NAV로는 회차 정산을 막는다 | 🟡 타임락 | ✅ |
차원은 아니지만 이 페이지 전체를 지배하는 규칙이 둘 있다. NAV는 항상 Aset 오라클이 정한다, 그리고 수익 분배는 수동으로만 한다.
자금 흐름과 커스터디
투자자 입금이 어디로 가고 자금이 어떻게 보관되는지를 설정 셋이 정한다.
| 설정 | 타입 | 기본값 | 변경 가능성 | 상태 |
|---|---|---|---|---|
fund_wallet | TEXT (주소) | NULL(배포 시 필수) | 🟡 타임락 | 📋 스펙 |
reserve_bps | INTEGER | 1000 | 🟡 타임락 | ✅ 코드 |
is_showcase | BOOLEAN | false | 🔒 불변 | 📋 스펙 |
fund_wallet
각 입금의 파트너 잔여분(amount − reserve)이 가는 외부 지갑이다. 다음 중 하나일 수 있다.
- 파트너 지갑(예: Joob은 Henon, LINE BK는 LFC SPV)
- 멀티시그 컨트랙트(Gnosis Safe 등)
v3-26: 반드시 파트너가 통제해야 한다(Aset 직접 풀 없음)
fund_wallet은 Aset 트레저리여서는 안 된다. fund_wallet이 Aset 트레저리인 풀은 모든 입금의 파트너 잔여분을 Aset으로 보내게 되고, 그러면 Aset이 투자자 자금을 보유하게 되어 수탁이 된다(비수탁 원칙과 VASP 회피가 깨진다). v3-26에 따라 모든 풀은 fund에 연결되고(fund_id 필수) 잔여분은 파트너 통제 아래 남는다. Aset 직접 구성은 제거됐다. 거부된 예시 1을 참조.
온체인 init에서 강제한다. PoolConfigLib.validate는 fundWallet == treasuryWallet이면 FundWalletIsTreasury()로 revert한다(PoolConfigLib.sol:60). 그래서 팩토리를 직접 호출해도 우회할 수 없다. ⚠️ 백엔드는 표시 전용이 아닌 모든 풀에 fund_wallet이 존재할 것만 요구하고(pools.post.create.ts:647) 트레저리와 비교하지는 않는다. 따라서 배포 시 revert가 유일한 게이트이고, 이는 배포가 없는 is_showcase 풀은 그 게이트를 한 번도 만나지 않는다는 뜻이기도 하다.
is_showcase = false일 때 필수다. 풀 배포 시 설정한다. 변경하려면 멀티시그 어드민 제안과 7일 타임락이 필요하다.
변경 메커니즘: 어드민(멀티시그)이 proposeFundWalletChange(newWallet)를 호출하면 FundWalletChangeProposed(oldWallet, newWallet, effectiveAt) 이벤트가 발생한다(GovernanceLib.sol:291). 7일이 지나면 누구나 executeFundWalletChange()를 호출할 수 있고, 그 기간 안에는 멀티시그가 cancelFundWalletChange()로 취소할 수 있다. 파트너는 그 이벤트를 구독해서 변경을 알게 된다. 스마트 컨트랙트 → 파트너 통지 참조.
reserve_bps
각 입금 중 유동성 버퍼로 Pool 컨트랙트에 남기는 비율(bps)이다. 파트너 자금이 아직 없을 때 상환을 즉시 지급하는 데 쓴다.
- 기본값: 1000(= 10%)
- 범위: 0에서 5000(5000 bps, 즉 50%를 넘는 경우는 드물다)
- 정수 bps로 저장한다(1% = 100 bps, 100% = 10000 bps)
- 입금 시 사용:
reserve = deposit_amount × reserve_bps / 10000
is_showcase
⚠️ 0203에서 is_display_only에서 이름이 바뀌었고 의미도 좁아졌다. 이 컬럼은 예전에 아래의 커스터디 성격까지 정의했는데, 그 부분은 custody_mode로 옮겨 갔다. 여기 남은 것은 v3-40 마케팅 등급이고, 프론트엔드가 실제로 구현한 의미다.
true이면 풀은 보이지만 여기서는 절대 투자할 수 없다. 투자자 PDP는 Overview 탭만 렌더하고 투자 패널 자리에 "Preview / Coming soon / Register interest" 카드를 넣는다(pool.$id.tsx가 그 분기에서 v3-40을 인용한다). 대시보드의 발견 목록에서도 제외된다.
true일 때의 동작:
deposit()도 재투자도 없다- 생성 시 컨트랙트를 배포하지 않고,
fund_wallet은 금지된다(자금 경로가 없다) - Performance 탭과 Fund Data 탭이 숨겨진다. 그러니 파트너 수치를 보여 주려고 등재한 풀에는 쓰면 안 된다
생성 시 설정하고 이후에는 바꿀 수 없다. 생성이 배포와 묶여 있어서, 나중에 뒤집으면 배포된 적 없는 풀이 배포됐다고 주장하게 된다.
설정한 풀이 하나도 없다(이름을 바꿀 당시 dev 8개 중 0개). v3-40 등급은 명세되고 배선까지 됐지만 한 번도 렌더된 적이 없다.
custody_mode
Aset이 이 풀의 자본을 보관하고 처리하는지 여부다. PLATFORM(기본값. 지금까지 한 개를 뺀 모든 풀)이거나 MIRROR다.
MIRROR는 포지션이 플랫폼 바깥에 존재하고 우리가 파트너 보고를 미러링한다는 뜻이다. 예를 들어 Joob의 Kaia 컨트랙트에 있는 ShardLab/Hashed의 RPS 보유분이 그렇고, 그 풀이 FJL IDN Private Credit 1이다. 우리를 통한 자본 유출입 경로가 없다. 입금, 재투자, 상환 요청, 수익 청구가 모두 거부되고(business/custody-mode.ts), 어드민 변경 경로 23개도 거부한다. 읽기 모델은 의도적으로 다른 풀과 완전히 같다. 같은 원장, 같은 fold, 같은 프로젝션, 같은 페이로드를 쓰고 이벤트의 origin만 다르다(0198). NAV는 제안이 아니라 기록되며 nav_history.attested_off_chain으로 표시된다(0201).
미러 풀도 fund_wallet과 reserve_bps를 갖고 풀 주소도 갖는다. 그래서 is_showcase를 재사용할 수 없었다. is_showcase의 생성 경로는 그것들을 전부 금지하기 때문이다.
API로는 설정할 수 없다. custody_mode는 어떤 생성·수정 핸들러에도 등장하지 않고 마이그레이션만이 값을 넣는다. 풀을 미러로 만들거나 미러를 해제하는 것은 의도적으로 어드민 조작 대상이 아니다.
is_hidden: 노출 여부만 바꾼다 (v3-110 A)
부담 없는 노출 토글이다. 🟢 어떤 상태에서든 언제든 되돌릴 수 있고, 온체인 구성요소가 없으며, 풀이 할 수 있는 일도 달라지지 않는다. 포지션을 보유한 사람에게 입금, 상환, 수익 청구, NAV 갱신, 알림은 그대로 유지된다.
⚠️ 구현된 것은 어드민 목록 필터뿐이다. 투자자 목록과 PDP까지 포함한다고 적힌 결정문보다 좁다. pools.get.list는 operator 분기 안에서만 .eq('is_hidden', false)를 적용하고(?include_hidden=true로 다시 포함할 수 있다), 투자자 PDP는 숨겨진 풀에 대해 404를 내지 않는다. 홀더는 상환하고, 수익을 청구하고, 상각 안내를 읽으려면 그 풀에 접근할 수 있어야 한다.
is_showcase(🔒 불변, v3-40 마케팅 등급), custody_mode(🔒 Aset이 이 풀의 자본을 보관·처리하는지 여부), 아카이브(deleted_at. 종료에 가까운 lifecycle이고 남은 포지션이 0일 때만 가능하며 온체인 pause()를 반영하고 되돌리려면 사유가 필요)와는 다르다. 셋의 전체 비교와 아카이브를 좁힌 이유는 상태 머신 → 숨김 축과 v3-110 A에 있다.
자산 설정
기초자산, 통화 처리, 가치 측정 방식을 설정 일곱 개가 서술한다.
| 설정 | 타입 | 기본값 | 변경 가능성 | 상태 |
|---|---|---|---|---|
chain_id | INTEGER | 8453 (Base) | 🔒 불변 | ✅ 코드 |
accepted_currencies | TEXT[] | ['USDC'] | 🔒 불변(첫 입금 이후) | ✅ 코드 |
operating_currency | TEXT | 'USD' | 🔒 불변 | ✅ 코드(pools.post.create / .patch.update에서 받는다. 트랜치 그룹 구성원끼리는 값이 같아야 한다) |
fx_rate_source | ENUM | FIXED | 🟢 어드민(DRAFT에서만) | ✅ 코드(생성 시 enum 검증). ⚠️ USD가 아닌 operating_currency인데 fx_rate_source가 비어 있는 경우를 막는 가드가 아직 없다 |
collateral_description | TEXT | NULL | 🟢 어드민(즉시) | 📋 스펙 |
collateral_ratio | NUMERIC | NULL | 🟢 어드민(즉시) | 📋 스펙 |
chain_id
풀이 어느 블록체인에 배포되는지다. 스테이블코인 주소, 블록 익스플로러, 컨트랙트 배포 체인이 여기서 정해진다.
현재 지원 목록:
1: Ethereum 메인넷8453: Base 메인넷8217: Kaia 메인넷- 테스트넷:
11155111(Sepolia),84532(Base Sepolia)
배포 후에는 바꿀 수 없다. 컨트랙트가 물리적으로 그 체인에 존재하기 때문이다. 체인을 바꾸려면 새 풀을 배포해야 한다.
accepted_currencies
이 풀이 입금으로 받는 스테이블코인 배열이다. TEXT[]로 저장하고(예: ['USDC', 'USDT']) 기본값은 {USDC}다. 지원 집합은 USD 기반 스테이블코인뿐이다. USDC, USDT, DAI(currency enum). USDC가 기본이자 권장값이고, 한 풀이 이 중 여럿을 받을 수도 있다.
풀당 스테이블코인 하나를 권장한다(여러 USD 기반 코인도 지원)
USD 기반 스테이블코인 여럿(USDC / USDT / DAI)을 지원하고, 풀의 accepted_currencies에 여러 개를 넣을 수도 있다. 다만 실무에서는 풀당 하나를 권장한다. 투자자는 자기가 입금한 통화로만 수익을 받을 수 있어서, 여러 스테이블코인을 받는 풀은 파트너가 여러 통화로 비율에 맞춰 수익을 보내야 한다(복잡하다).
여러 스테이블코인을 받고 싶은 파트너는 풀을 따로 배포한다(예: LINE_BK_USDC, LINE_BK_USDT).
체인별 컨트랙트 주소는 STABLECOIN_ADDRESSES[chainId][symbol] 조회로 해석한다.
발행사가 직접 배포한 네이티브 스테이블코인만 지원한다. 브리지 버전(예: USDC.e)은 디페그 위험 때문에 제외한다.
operating_currency
기초자산이 표시되는 통화다. 백엔드 전용이다. 화면에 보여 주는 USD 환산 참고값과 상환 시 적용하는 환산에 쓴다. 투자자는 USD(USDC)로 입금하고 상환받으며 현지 통화를 보유하지 않으므로 직접적인 환위험이 없다. 따라서 투자자 대상 FX 고지도 없다(아래 참조).
- 기본값:
'USD' - 예시:
'IDR'(Joob, 인도네시아 루피아),'THB'(태국 바트),'MYR'(말레이시아 링깃) - enum이 아니라 자유 텍스트다. 파트너를 온보딩하면서 통화를 추가한다.
accepted_currencies와 다르다. 투자자는 USD 페그 스테이블코인(USDC)으로 입금하지만, 기초 펀드는operating_currency로 운영된다.
여기서 규칙 둘이 따라 나오는데 둘 다 다른 문서가 소유한다. FX는 NAV 입력이 아니다. NAV는 운영 통화 기준으로 자산 성과를 재고, USD 환산은 상환 시점에 일어난다(R7, 동작은 06 → FX는 NAV에 들어가지 않는다). 그리고 투자자 대상 FX 고지나 배너는 없다. USD로 들어오고 USD로 나가므로 직접적인 환위험이 없기 때문이다(v3-24, 17-changelog).
fx_rate_source
operating_currency와 USD 사이의 환율을 어떻게 정하는지다.
| 값 | 뜻 |
|---|---|
FIXED | 풀 배포 시 정한 고정 환율(예: Joob은 16,730 IDR/USD). 바뀌지 않는다. |
EXTERNAL_FEED | 파트너 API에서 매일 가져온다(예: Joob API가 현재 환율을 반환). |
온체인 FX 오라클은 쓰지 않는다. IDR/THB/MYR에 대한 Chainlink·Pyth 커버리지가 부실하고, 온체인 FX는 이득 없이 복잡도만 늘린다. 실시간 환산이 필요한 파트너가 생기면 다시 검토할 수 있다.
NAV 처리: 항상 Aset 오라클 (v3-15)
nav_data_source 차원은 제거됐다. 모든 풀이 Aset 오라클 Lambda로 NAV를 계산한다.
- 원천 데이터는 파트너 API(Joob eNote, LINE BK 리스크 대시보드)나 어드민 폼에서 들어온다
- Aset이 상각 스케줄(v3-13), 트랜치 워터폴(v3-14), 자체 평가 산식을 적용한다
- Aset이 온체인
updateNAV()를 호출한다(오라클 role)
이로써 "순수 미러링" 경로가 사라졌다. 파트너 제안은 여전히 입력으로 들어올 수 있지만, 최종 NAV는 항상 Aset이 판단한다.
collateral_description
풀의 대출·자산을 무엇이 뒷받침하는지 서술하는 자유 텍스트다. 투명성을 위해 투자자에게 보여 준다.
예시:
"Indonesian EWA payroll receivables"(Joob)"Thai SME invoice receivables"(LINE BK 태국 후보)"Tokyo real estate"(가상의 부동산 풀)NULL: 미지정(드물다. 담보 내용이 기밀일 때만 쓴다)
어드민이 언제든 수정할 수 있다(예: 파트너가 자산 정보를 보완해 준 경우).
collateral_ratio
미상환 대출 원금 대비 담보 가치의 비율(percent)이다.
- 형식:
100 = 100%(1:1 담보) - 예시:
150이면 150% 초과담보 ·80이면 80% 부분담보 ·NULL이면 무담보이거나 미지정 - 선택값이다. 담보가 정성적으로만 서술되는 경우(설명으로 충분한 경우) NULL로 둘 수 있다
리스크 판단을 위해 collateral_description과 함께 투자자에게 보여 준다.
수익 설정
수익이 투자자에게 어떻게 분배되는지를 설정들이 정한다.
| 설정 | 타입 | 기본값 | 변경 가능성 | 상태 |
|---|---|---|---|---|
yield_frequency | ENUM | MONTHLY | 🟢 어드민(즉시) | ✅ 코드 |
net_yield_fee_config | JSONB | NULL | 🟢 어드민(즉시) | 📋 스펙 |
수익 분배 트리거: 수동만 (v3-20)
모든 풀에서 수익은 claim 기반이자 수동 트리거다. 파트너가 수익을 입금하고(depositYield), Aset이 pro-rata로 정산하며(settleYield), 투자자가 직접 청구한다(claimYield()). 자동 분배 경로도 없고 트리거 컬럼도 없다. yield_trigger ENUM은 v3-20에서 삭제됐다(11-db-schema · 17-changelog).
풀이 약속한 주기는 아래 yield_frequency에 있다. 자동화 트리거가 아니라 공개된 약속이다.
yield_frequency
수익을 얼마나 자주 분배하기로 했는지다. 투자자에게 약속으로 표시된다.
| 값 | 뜻 | next_yield_due 전진 폭 |
|---|---|---|
MONTHLY | 월 1회 | 역월 한 달, 연 12회(addUtcMonths, 월말 클램프: 1월 31일 + 1개월 = 2월 28일 또는 29일) |
QUARTERLY | 분기 1회(Joob 기본값) | 역월 3개월 |
CUSTOM | 그 외 주기(custom_interval_value + custom_interval_unit) | 값 × 단위. 단위가 months면 역월 기준 |
어드민 위저드가 제공하는 것은 이 셋이다. 컬럼은 CHECK 없는 순수 TEXT이고, 백엔드는 행에 값이 들어 있으면 WEEKLY / ANNUAL도 인식하며 모르는 값은 MONTHLY로 폴백한다. 다만 제품에서 그 값들을 설정할 방법이 없으므로 위 표를 전체 집합으로 보면 된다.
이 축이 "월"이 역월을 뜻하는 유일한 곳이다. 30일도 아니고 회차 주기의 28일도 아니다. 셋은 별개의 시간 기준이고, 용어집 → 한 달에서 한 번에 정리했다(v3-124). 그래서 분배일은 밀리지 않고 같은 날짜를 유지하는데, 월말 NAV와 명세서 주기가 이것에 의존한다.
분배는 항상 수동 트리거지만, yield_frequency는 약속으로 투자자에게 보인다. 한 번 놓치면 yield_overdue = true가 되고, next_yield_due = 마지막으로 지급한 기간의 종료일 + 주기가 되며, 어드민과 투자자에게 알림이 나간다. 풀이 자동으로 pause되지는 않는다(v3-94).
net_yield_fee_config
총 수익에서 몫을 떼어 가는 파트너를 위한 수수료 구조를 담은 JSONB 컬럼이다. 파트너 수수료가 없는 풀은 NULL이다.
키는 bps(1% = 100 bps)이고 수령 주체별로 묶인다(v3-69).
| 키 | 수령 주체 |
|---|---|
platform_yield_take_bps · spc_mgmt_bps · perf_fee_bps(임계값인 perf_hurdle_bps는 수수료가 아니다) | Aset 트레저리 |
pool_mgmt_bps | fund_fee_wallet. 펀드의 수수료 수령 주소이고 입금 money-path의 fund_wallet과는 다른 주소다 |
sourcing과 ramping은 오프체인 운영비이고 이 설정에 포함되지 않는다. 금액은 Lambda가 오프체인에서 계산하며(컨트랙트는 요율을 저장하지 않는다), 수수료 두 갈래와 홀더에게 가는 net이 settleYield 호출 한 번에 함께 움직인다(v3-102).
계산 예시, 수령처 표, 계산이 오프체인인 이유는 수익 동작 방식 → Net Yield 계산 참조.
투자자 조건
투자자 경험을 설정 여덟 개가 정한다. 자금이 얼마나 묶이는지, 풀이 언제 만기가 되는지, 그 전에 이탈할 수 있는지, 조기 이탈에 어떤 패널티가 붙는지, 트랜치가 수익을 어떻게 나누는지, 수익을 청구 대신 재투자할 수 있는지다.
| 설정 | 타입 | 기본값 | 변경 가능성 | 상태 |
|---|---|---|---|---|
lockup_days | NUMERIC | 0 | 🟢 DRAFT에서만, ACTIVE 이후 🔒 | ✅ 코드 |
maturity_model | ENUM | FIXED_TERM | 🔒 불변 | 📋 스펙 |
maturity_days | NUMERIC | NULL | 🟢 DRAFT에서만, ACTIVE 이후 🔒 | ✅ 코드 |
redemption_type | ENUM | ON_DEMAND | 🔴 생성 시에만(온체인) | ✅ 코드 |
penalty_type | ENUM | NO_EARLY | 🟢 DRAFT에서만, ACTIVE 이후 🔒 | ✅ 코드 |
tranche_group_id | UUID NULL | NULL | 🔒 불변 | 📋 스펙 |
tranche_role | ENUM NULL | NULL | 🔒 불변 | 📋 스펙 |
allow_rollover | BOOLEAN | false | 🟢 어드민(즉시) | ⚠️ MVP에서는 어느 화면에도 노출하지 않는다(2026-08-27, v3-151) |
lockup_days
입금 후 패널티 없이 상환하려면 얼마나 기다려야 하는지다. 0이면 락업이 없다.
- 기본값: 0
- 예시:
90(Joob의 3개월 락업),30,180 - 어느 기준점을 쓰는지는 풀 구현체의 속성이고, 두 기준이 영구히 공존한다(
Clones). pool-wide 구현체에서는lockup_end = subscription_end_date + lockup_days로 모든 홀더가 같은 날이고, 그 이전 구현체에서는 홀더별 입금일이 기준이다. pool-wide에서는 세 값이 생성 시에만 설정 가능하고 한 세트로 검증된다. 모집 마감일 없는 락업이나 기간 종료 후에 끝나는 락업은 풀 생성 시 거부되므로, 그런 풀은 마감된 모집을 다시 열 수도 없다. 07 → 3-State Lockup Model 참조 - 🔴 OPEN_ENDED 풀에는 락업이 아예 없다(v3-146, JY 2026-08-25). FIXED_TERM 풀은 어차피 모집 마감일이 필요하고(기간을 그 시점부터 재므로) 락업에도 항상 기준점이 있다. 반면 개방형 풀의 모집 창은 선택값이다. 만기가 없는 풀에 창을 비워 두는 것은 정당한 답이고, 그러면 pool-wide 락업이 셀 날짜가 없다. "예정된 종료가 없다"는 게 본질인 풀에 마감일을 요구하거나, 아무도 기록하지 않는 두 번째 기준점을 만들어 내는 대신, 그 형태에서는 필드를 없앤다.
lockupApplies가 판정하고, API는 생성과 수정에서 이 조합을 거부하며, 체인도 거부한다(LockupRequiresSubscriptionEnd).- 결과적으로 개방형 풀에는 출구 게이트가 하나도 없다. 패널티는 "무엇보다 이른가"를 정할 만기가 있어야 하는데(
maturityDate가 없으면isBeforeMaturity가 false다), 그래서 락업이 유일한 게이트였다. dev의 모든 개방형 풀은 이미 그렇게 돌고 있다(락업 0,NO_EARLY). 최소 보유 기간은 포지션별 라운드로 미뤘다. - ⚠️ 거부되는 이유는 개방형 풀이라서가 아니라 기준점 때문이다. 만기가 포지션별이 되면 락업도 홀더별 입금일로 돌아가고(모든 풀이 그 값을 갖는다), 이 제약은 v3-144의 재개방 금지와 함께 저절로 풀린다.
- 결과적으로 개방형 풀에는 출구 게이트가 하나도 없다. 패널티는 "무엇보다 이른가"를 정할 만기가 있어야 하는데(
- v3-83:
lockup_days는penalty_type과 독립이다.NO_EARLY풀도 락업을 가질 수 있다(락업 중에는 LOCKED, 이후에는 패널티 없이 이탈). 패널티는 EARLY 구간(락업 후, 만기 전)에만 적용된다.penalty_type참조(v3-66을 대체한다)
maturity_model
풀이 자산 만기를 어떻게 다루는지다.
| 값 | 뜻 |
|---|---|
FIXED_TERM | 풀에 종료일이 있다. 만기 후에는 모든 투자자가 패널티 없이 상환할 수 있고, 풀은 MATURED로 넘어간다. |
OPEN_ENDED | 기초자산이 순환한다(예: 3~6개월 대출이 만기마다 교체된다). 풀 자체는 더 긴 기간 운영된다(예: LINE BK PoC 6개월). 제품·UX 문구에서는 "revolving"이라고 부른다. |
저장되는 enum 값은 **OPEN_ENDED**다(DB, 생성·수정 핸들러, 컨트랙트 모두). "REVOLVING"은 표시·UX 용어일 뿐이고, API에 maturity_model: "REVOLVING"을 보내면 400이 돌아온다.
기초자산 모델을 보고 고른다.
- 종료일이 있는 단일 펀드 →
FIXED_TERM - 자산이 순환하는 연속 대출 운영 →
OPEN_ENDED
maturity_days
풀 시작부터 만기까지의 일수다. maturity_model = FIXED_TERM일 때만 의미가 있다.
- 기본값: NULL(
FIXED_TERM이면 반드시 설정해야 한다) - 예시:
365(Joob의 1년),180(6개월 PoC),730(2년) maturity_days는 기간 길이이지 만기가 아니다. 만기 자체는 절대 시각인pools.maturity_date이고, 배포 시subscription_end_date + maturity_days로 기록되며(v3-145. 2026-08-24 이전에는deploy_time + maturity_days였다) 컨트랙트의maturityDate를 미러링한다(마이그레이션 0161). 이는 보유분이 아니라 풀의 속성이다.invested_at에서 다시 계산하지 말 것. 🔴 기간을 모집 마감일부터 재므로 FIXED_TERM 풀에는 마감일이 반드시 있어야 한다. 마감일이 없으면 만기로 삼을 날짜가 없고, 발행 엔드포인트와 배포 워커 둘 다 거부한다. 🔴 배포는maturity_date를 쓰고end_date는 건드리지 않는다. 그래서 배포 전에 대기한 풀은 대기한 기간만큼 두 값이 영구히 어긋난다.end_date에는 위저드 추정치가 남고maturity_date가 실제 기간 종료를 담는다.resolveMaturityAt이maturity_date를 우선하므로 동작은 정상이지만, 어드민 Configuration 탭의 Maturity 행은end_date를 읽으므로 어긋난 풀은 지키지도 않는 날짜를 표시한다. 배포가 결정하는 날짜가 만기뿐인 이유이기도 하다.start_date와subscription_end_date(0204)는 위저드에서 쓰는 절대 날짜라 움직이지 않으므로, 모집 마감과 만기 사이 간격이 지연에 대해 안정적이지 않다. 모집 마감일을 지나서 배포하면 매시간 도는 마감 sweep이 입금이 들어오기도 전에 신규 자금을 막아 버리는 풀이 나가는데, 이제 발행 경로와 재시도 경로 둘 다 이를 거부한다(v3-141).
결정됐으나 미구현: 만기가 투자자별이 된다
풀 단위 만기는 모집 마지막 날에 들어온 홀더와 첫날에 들어온 홀더가 같은 날 만기가 된다는 뜻이고, 짧게 보유한 사람이 온전한 기간의 쿠폰을 받는다는 뜻이다. 결정은 lockup_days가 이미 그렇듯 만기도 포지션별 기준점을 갖게 하는 것이다(3-State Lockup Model). 다만 컨트랙트는 절대 시각 maturityDate를 저장하고 maturityDays가 없으므로 이건 컨트랙트 변경이고, 아래 내용은 아무것도 배선되지 않았다.
MVP에는 없다. 임시 대응은 **짧은 모집 기간(2주 안팎)**이고, 이렇게 하면 편차가 모집 창 길이로 제한된다. 컨트랙트 배포 라운드가 열리면 정산일 기준 수익 종료와 같은 라운드에 넣는다. 둘 다 컨트랙트를 건드리기 때문이다. 전체 스펙과 추정치는 Notion "만기 기준을 풀 단위 → 투자자별로 전환"에 있다.
redemption_type
풀이 만기 전 이탈을 애초에 받아들이는지 여부다. 온체인 값(RedemptionConfig.redemptionType)이고 생성 시 고정된다.
| 값 | 온체인 | 뜻 |
|---|---|---|
ON_DEMAND | 1 | 3-State 모델이 허용하는 한 언제든 이탈을 받는다. 락업 이후이고, EARLY 구간에서는 penalty_type이 적용된다 |
FIXED_MATURITY | 0 | 만기 전까지 이탈을 거부한다. RedemptionLib이 block.timestamp < maturityDate인 동안 revert하고, maturityDate == 0인 풀은 무기한 거부한다 |
LIQUIDITY_WINDOWS라는 세 번째 값이 있었으나 없어졌다(마이그레이션 0106). 그 이름이 가리키던 창 기반 동작은 epoch_duration_days > 0이고, 그건 독립적인 축이다.
⚠️ epoch_duration_days와 같은 축이 아니다. redemption_type은 만기 전 이탈이 가능한지를 정하고, epoch_duration_days는 받아들인 이탈을 어떻게 처리하는지를 정한다. 후자는 EARLY 구간과 만기 후 기간 모두에 같은 값이 적용된다. 온체인에서 둘은 직교한다. 플랫폼이 실제로 표현할 수 있는 조합은 상환 → 구간 × 모드를 보라.
penalty_type
EARLY 구간(락업 후, 만기 전)에 어떤 패널티가 붙는지다. v3-83에 따라 penalty_type은 lockup_days와 독립이다(아래 주의 참조). 어떤 타입이든 어떤 락업과도 조합할 수 있다.
| 값 | 뜻 |
|---|---|
NO_EARLY | 조기 이탈 패널티 없음(요율 0). 락업과 독립이다. lockup_days = 0이면 언제든 무료 상환, lockup_days > 0이면 락업 중에는 잠기고 그 뒤 무패널티 이탈. |
FLAT_FEE | 고정 금액을 뗀다(예: 상환 건당 $50). |
PRINCIPAL_BASED | 원금의 일정 비율을 뗀다(예: 상환액의 5%). |
YIELD_BASED | 발생 수익의 일정 비율을 몰수한다(예: Joob의 배당 50% 몰수). |
조기 이탈 패널티는 풀의 fund_wallet으로 이전된다(v3-85). 더 이상 reserve에 쌓이지 않는다. 전체 메커니즘은 상환 참조.
⚠️ NO_EARLY는 조기 이탈 패널티가 0이라는 뜻이지, "조기 상환 불가"나 "락업 없음"이 아니다. 어떤 lockup_days, 어떤 redemption_type과도 조합할 수 있다. (v3-66은 둘을 묶어 온체인에서 강제했는데, 그 가드는 제거됐다. v3-83)
트랜치 그룹 (v3-14)
트랜치 상품은 tranche_group_id를 공유하는 별도의 SINGLE 풀 2~3개를 배포한다. 각 풀은 tranche_role을 갖는다.
| 값 | 뜻 |
|---|---|
| NULL | 단독 풀(트랜치 없음). 기본값이다. |
SENIOR | 리스크가 가장 낮고 수익도 가장 낮다. 손실을 마지막에 흡수한다. |
MEZZANINE | 중간 계층(선택. 총 3계층까지). |
JUNIOR | 리스크와 수익이 가장 높다. 손실을 먼저 흡수한다. 보통 파트너가 댄다(LFC의 first-loss 자본). |
그룹 구성 방식, 같은 그룹 요건, 제약, Junior 소진
그룹 구성 방식:
- 어드민이 같은
tranche_group_id와 서로 다른tranche_role을 가진 풀 2~3개를 만든다. - 배포 시 아래의 같은 그룹 제약을 검증한다.
- 투자자는 원하는 트랜치에 입금한다.
- Aset 오라클 Lambda가 기초 펀드 전체에 대한 파트너 데이터(자산 가치, DPD, 손실)를 가져온다.
- Lambda가 손실 워터폴(Junior → Mezzanine → Senior)을 적용하고 각 풀의 NAV를
Pool.updateNAV()로 따로 갱신한다. - 투자자는 자기 풀의 NAV로 자기 풀에서 상환한다. 온체인에서 풀 간 상호작용은 없다. 트랜치 그룹 워터폴 참조.
같은 그룹 요건(팩토리 배포와 어드민 UI 검증에서 강제):
- 일치해야 하는 값:
fund_wallet,operating_currency,chain_id,external_provider,maturity_days - 달라도 되는 값:
reserve_bps,apy_rate,jurisdiction_whitelist,lockup_days,penalty_type,penalty_rate_bps
제약:
tranche_group_id당 최대 3개 풀(역할당 하나)- (
tranche_group_id,tranche_role)에 유니크 제약. 그룹 안에 중복 역할 불가 - 한 풀은 최대 하나의 그룹에 속한다(
tranche_group_id는 단일 값)
Junior 소진 시 동작: Junior 풀의 NAV가 0에 닿으면 자동으로 IMPAIRED로 전이한다(v3-12). 입금은 멈추고, 상환은 락업이 면제된 채 열려 있다. 파트너가 자본을 보충하면 NAV가 회복되고 ACTIVE로 돌아온다. 그러지 않으면 결국 청산으로 간다.
배포 후에는 tranche_group_id도 tranche_role도 바꿀 수 없다.
allow_rollover
투자자가 발생 수익을 지갑으로 청구하는 대신 풀에 재투자할 수 있는지 여부다.
🔴 MVP에서는 어느 화면에도 노출하지 않는다. 2026-08-27 결정(v3-151)으로 재투자를 투자자 앱과 어드민(펀드매니저 포함)에서 뺀다. 아직 반영되지 않았다. ⚠️ 온체인 reinvest()와 POST /yield/reinvest는 둘 다 남는다. 실제로 경로를 닫는 것은 버튼 제거가 아니라 이 필드의 기본값 false다.
- 기본값:
false true일 때: 투자자가reinvest()를 호출해 발생 수익으로 LP를 추가 민팅할 수 있다(패널티 없음. 락업 기준점은 입금과 정확히 동일하다. v3-130)- 전체 규칙은 투자 라이프사이클 → Reinvest V1 참조
컴플라이언스
누가 투자할 수 있는지는 관할과 유효한 SBT로 게이팅한다. 풀 단위 KYC/KYB 레벨 게이트는 없다. SBT가 레벨을 저장하긴 하지만 입금을 막지 않는다. 투자자 적격성은 백엔드에서 pools.eligibility_mode(STATUS / MIN_TICKET)와 users.investor_status로 처리하고 checkKycGating이 강제한다(v3-74 · 03-kyc-identity).
| 설정 | 타입 | 기본값 | 변경 가능성 | 상태 |
|---|---|---|---|---|
enforce_jurisdiction | BOOLEAN | false | 🟡 타임락 | ✅ 코드 |
jurisdiction_whitelist | TEXT[] (ISO 코드) | [](전체 허용) | 🟡 타임락 | ✅ 코드 |
allows_us_persons | BOOLEAN | false | 🟡 타임락 | ✅ 코드 |
enforce_jurisdiction과 allows_us_persons
enforce_jurisdiction(0064)이 마스터 스위치다. 꺼져 있으면 국가 제한이 없고, 켜져 있으면 화이트리스트가 적용된다(화이트리스트가 비어 있는데 enforce가 켜져 있으면 전부 차단이다). allows_us_persons는 기본값이 false라 미국 국가 코드를 가진 투자자를 거부한다. 미국인 여부는 별도 플래그가 아니라 SBT의 countryCode에서 파생된다.
jurisdiction_whitelist
입금이 허용되는 ISO 3166-1 alpha-3 코드다(['KOR', 'SGP']이면 그 둘만, []이면 전체 개방). alpha-3가 SumSub에서 DB, SBT, 온체인 검증까지 전 구간의 정본이다. 컨트랙트가 keccak256(화이트리스트 코드)와 keccak256(SBT.countryCode)를 비교하기 때문에, "KR"은 저장된 "KOR"과 절대 일치하지 않는다. 남은 오프체인 정리 항목은 v3-60에 있다.
풀별로 온체인에서 강제한다. requiresKYC 모디파이어가 JurisdictionNotAllowed로 revert하고, 여기에 isValidKYCNonUS()의 미국인 검사가 더해진다(08a → §3). 트랜잭션 전에 API 계층(deposits.post.create가 kyc-gating.ts를 통해)에서도 같은 검사를 해 방어를 겹친다. 변경하려면 멀티시그와 7일 타임락이 필요하다.
KYC & 신원 참조.
풀 상태
런타임 설정 셋이 풀의 lifecycle과 운영 상태를 추적한다. 배포 시 "설정"하는 값이 아니라 풀이 사는 동안 변하는 값이다.
| 설정 | 타입 | 기본값 | 변경 가능성 | 상태 |
|---|---|---|---|---|
lifecycle_status | ENUM | DRAFT | 자동 + 어드민 트리거 | ✅ 코드 |
is_paused | BOOLEAN | false | 🟢 어드민(즉시) | ✅ 코드 |
is_emergency_frozen | BOOLEAN | false | 🟢 어드민(즉시) | ✅ 구현 완료(freeze/unfreeze 핸들러 · freeze_started_at 마이그레이션 0023 · 컨트랙트 미러) |
lifecycle_status
풀의 현재 단계다.
| 값 | 뜻 |
|---|---|
DRAFT | 풀은 만들어졌지만 아직 배포되지 않았다. 설정 변경이 가능하다. |
UPCOMING | 배포됐고 모집 창은 아직 열리지 않았다. |
ACTIVE | 모집이 열려 있다. 입금과 상환이 허용된다. |
IMPAIRED(v3-12) | 파트너 부실. 입금이 멈추고 상환은 열려 있다. ACTIVE로 회복 가능하다. |
CLOSED | 모집이 끝났다. 기존 투자자만 남고 신규 입금이 없다. 상환과 수익 청구는 계속된다. POST /pools/{id}/close로 가거나 pools.scheduler.lifecycle이 end_date에 자동으로 보낸다. 다시 열 수 있다(action: reopen). ✅ 반영 완료(v3-110 B). |
MATURED | 만기가 왔다. 모든 상환이 패널티 없이 허용된다. |
WIND_DOWN | 종착 상태다. NAV가 풀이 실제로 보유한 자산의 pro-rata 지분으로 강제되고, 이탈은 requestRedemption → claim 경로로만 진행된다. 독립적인 redeem()은 없다. 산식은 긴급 Wind-Down 참조. |
⚠️
ARCHIVED는 이 enum에 없다.deleted_at에서 파생되는 표시 라벨이고, 실제 lifecycle이 무엇이든 그 위에 겹쳐지는 세 번째 축이지 단계가 아니다.is_paused와 같은 부류다. 상태 머신 → 숨김 축과 v3-110 참조.
v3-40: Preview(showcase) 등급
풀을 마케팅·전시 항목으로 등재할 수 있다. 보이지만 Aset에서는 절대 투자할 수 없다(예: 파트너의 다른 펀드를 보여 주어 다음 라운드에 관심을 모으는 경우). preview 풀은 Overview만 보여 주고(개요 프로필과 딜 구조), Performance와 Fund Data는 숨긴다. "관심 등록" CTA를 단다(CLOSED 풀에도 같은 CTA가 붙는다). UPCOMING(곧 여기서 열린다)이나 CLOSED(여기서 열렸었다)와는 다르다. 표시 등급 Preview / Display / Full은 어떤 파트너 API 필드가 필요한지를 정한다(preview는 정적 프로필만). v3-40 참조.
상태별 상세와 전체 전이 표(proposeImpairment / cancelImpairment / proposeWindDown 타임락)의 정본은 상태 머신 § 풀 lifecycle에 있다.
is_paused
soft pause다. 어드민이 상환은 열어 둔 채 신규 입금만 일시 중단할 수 있다.
true일 때: 새deposit()호출이 revert된다. 기존 상환 요청은 정상 처리된다- 쓰는 경우: 일시적 문제(예: 파트너 동기화 장애), 컴플라이언스 검토, 시장 상황
- 투자자는 여전히 돈을 뺄 수 있다
is_emergency_frozen
hard pause다. 긴급 상황(컴플라이언스 이슈, 컨트랙트 버그, 보안 사고)에 모든 활동을 멈춘다.
true일 때: 모든 함수가 revert된다(입금, 상환, 수익 청구 등)- 쓰는 경우: 어드민 개입이 필요한 심각한 문제
- 해제될 때까지 투자자는 아무 동작도 할 수 없다
아껴서 쓸 것
is_emergency_frozen은 투자자가 자기 자금에 접근하는 것을 막는다. 진짜 긴급 상황에만 쓰고, 발동하면 투자자에게 명확히 알린다.
운영 / 고급 (선택)
상환 모델과 NAV 안전 한계를 포함한 운영 제어 설정이다.
| 설정 | 타입 | 기본값 | 변경 가능성 | 상태 |
|---|---|---|---|---|
epoch_duration_days | NUMERIC | 0 | 🔴 생성 시에만 | ✅ v3-26 / v3-38 |
redemption_gating_bps | INTEGER | NULL | 🟡 타임락 | ⚠️ 폐기됨. MVP에 포함되지 않는다(2026-08-27, v3-150) |
redemption_term_epochs | INTEGER | NULL | 🔴 생성 시에만 | 🔨 0189 (dev 적용 대기). FIXED_MATURITY + 회차 조합에 필수 |
epoch_date_basis | TEXT | NULL | 🔴 생성 시에만 | 🔨 0189 (dev 적용 대기). CALENDAR / FIXED_DAYS |
epoch_roll_day | SMALLINT | NULL | 🔴 생성 시에만 | 🔨 0189 (dev 적용 대기). CALENDAR에서 1~28 |
nav_deviation_cap_bps | NUMERIC | (설정값) | 🟡 타임락 | ✅ v3-32(핵심) |
nav_staleness_seconds | NUMERIC | (설정값) | 🟡 타임락 | ✅ v3-32(핵심) |
epoch_duration_days: 상환 모델 선택자 (v3-26)
풀이 이탈을 어떻게 처리할지 고른다. 입금은 어느 쪽이든 즉시·atomic이다.
0: 즉시 | > 0: 회차 | |
|---|---|---|
| 정산 | 요청 단위로, requestRedemption 트랜잭션 안에서 처리된다. reserve가 충분하면 즉시 정산되고, 모자라면 PENDING_RESERVE가 됐다가 파트너의 fundRedemption이 자동 정산한다. 어드민 승인 단계가 없다(v3-82) | 요청 창이 끝날 때 한꺼번에 처리한다. 이월분 우선 pro-rata 체결, 미체결 잔량 이월, pull claim |
| 가격 | 요청 시점 NAV로 고정 | forward pricing. 정산 시점 NAV |
| 추가 상태 | — | QUEUED · PARTIALLY_FILLED |
| 통상 기본값 | FIXED_TERM | OPEN_ENDED |
창은 고정된 N일 구간이 아니라 펀딩일에 앵커된 주기다(fundingAnchor + recallLeadDays + requestWindowDays). 창 밖의 요청은 거부되고 마감 시점에 수요가 고정된다. 전체 메커니즘과 정산 에스크로·스케줄 내부 동작은 07-redemption → Model B에 있다.
기본값은 생성 시 미리 채워 주는 값이지 제약이 아니다. epoch_duration_days는 maturity_model과 직교하고 둘 사이에 검증 결합이 없다. 그래서 런 리스크가 회차 방식의 공정성을 정당화하면 FIXED_TERM 풀도 > 0으로 만들 수 있다. 선택 기준은 만기 유무가 아니라 런 리스크다(v3-46). 어드민 위저드는 별개로 redemption_type = FIXED_MATURITY일 때 선택자를 숨기는데, 이건 위저드 계층의 가드일 뿐이다(v3-88 D7).
구현됐으나 미배포: 그 위저드 가드는 빠진다
D7은 만기가 된 풀은 한 번에 지급한다고 가정했고, 그러면 주기가 무의미하다고 봤다. 그런데 만기 후 여러 회차에 걸쳐 원금을 상환하는 풀이 정확히 그 조합이고, 체인은 늘 이를 허용해 왔다. 거부한 것은 그 가드뿐이었다. 이제 위저드는 FIXED_MATURITY + epoch_duration_days > 0을 허용하고, 스케줄 블록은 **"만기 후 상환"**으로 이름을 바꿨다. 이 조합에서는 그 조건들이 만기 이후에만 효력을 갖기 때문이다(v3-132).
⚠️ "컨트랙트 변경 없음"은 여전히 맞고, "스키마 변경 없음"은 틀렸다. 날짜를 바로잡는 데 마이그레이션 0189(계획 컬럼 3개)와 0190(redemption_epochs.funding_date)이 들었고 둘 다 dev에 적용되지 않았다. 체인은 건드리지 않았다. 함께 추가한 펀딩일 순서 가드는 GovernanceLib이 아니라 엔드포인트에 있다(v3-139). 스펙은 상환 → 만기 후 회차 상환에 있다.
생성 시에만 설정 가능하다(v3-38). 배포 멀티콜에 온체인 epochDurationDays로 포함되고 이후에는 수정 대상이 아니다. 이유는 변경 가능성 매트릭스에, 동기화 분류는 스마트 컨트랙트 → 필드 동기화에 있다.
redemption_gating_bps
⚠️ 폐기됨. MVP에 포함되지 않는다. 2026-08-27 결정(v3-150)이고 아직 반영되지 않았다. 화면과 API에서 이 상한을 빼기로 했는데, 어드민 폼과 투자자 문구에는 여전히 남아 있다. 컨트랙트와 DB 컬럼은 그대로 둔다. 스토리지, 거버넌스 setter, 이벤트가 다 남아 있고 이것은 제거가 아니라 라벨 변경이다. MVP에서 값은 항상 0이나 NULL이므로 아래 정산 산식에는 영향이 없다. ⚠️ 다른 게이트, 즉 투자자가 자기 LP 잔액만큼만 요청할 수 있다는 규칙(InsufficientLPTokens)은 별개이며 그대로 유효하다.
한 회차에 풀의 얼마만큼을 상환할 수 있는지에 대한 선택적 상한이다. 회차 풀에서는 체결 비율 계산 전에 available(상환 가능 reserve)을 gating_bps/10000 × totalDeposited로 제한한다(available = min(available, redemption_gating_bps × totalDeposited / 10000)). 즉시 풀에서는 향후 옵션이다. 기준은 totalDeposited, 즉 온체인 예치 자본 누계(누적 입금 + 재투자 수익 − 상환 지급액)이고 TVL(LP 공급량 × NAV)이 아니다. 상각이나 미분배 수익이 있으면 둘이 갈라지므로, 게이팅은 시장 가치가 아니라 장부 자본을 기준으로 잰다. 파트너가 보충하는 속도보다 reserve가 빨리 마르는 대량 상환(뱅크런) 상황을 막는다. (컨트랙트: RedemptionLib._epochFillRatio, v3-67.)
- 기본값: NULL(게이팅 없음)
- 예시:
500이면 회차당 풀 예치 자본의 최대 5%(500 bps)까지 정산한다. 초과분은 이월되거나(회차) 대기한다(즉시). - 선택 기능이고 기본은 꺼져 있다. 파트너가 요구할 때만 적용한다.
상환 계획: redemption_term_epochs · epoch_date_basis · epoch_roll_day
🔨 컬럼 세 개로 구현됨(0189), dev 미적용
⚠️ redemption_window_epochs는 스펙상의 이름이었고, 실제 컬럼은 redemption_term_epochs로 나갔다. 이 페이지와 16-timeline의 옛 문장은 아직 긴 이름을 쓰는데 같은 필드다. 원래 스펙이 예상하지 못한 규칙 컬럼 둘이 함께 들어왔다.
redemption_term_epochs는 만기 후 회차 풀이 몇 회차에 걸쳐 원금을 상환하는지다. 스케줄의 끝을 정하는 값이고, 시작점과 주기는 이미 fundingAnchor와 epoch_duration_days로 존재한다. 상환 스케줄을 서술하는 표준 3요소(ACTUS)에서 플랫폼에 없던 하나가 이것이었다.
- 회차 수로 저장하되, 입력 방식은 기준에 따라 다르다. ⚠️ 이 필드가 제안 단계일 때 이 페이지가 적었던 "항상 개월"이 아니다.
CALENDAR에서는 위저드가 개월로 묻는다. 날짜를 고정하는 것이 역법이기 때문이다.FIXED_DAYS에서는 회차 수로 묻는다. 28일 주기는 한 달이 아니라서, 4회차면 112일이고 이는 정수 개월이 아니다. 개월로 표기하면 스케줄에 존재하지 않는 숫자를 찍게 된다(v3-132). - NULL이면 무제한이고, 기존 회차 풀은 전부 그 상태다. 필드를 추가해도 그들의 동작은 바뀌지 않았다.
- 분할 금액을 정하지는 않는다. 각 회차는 파트너가 그 회차에 넣은 만큼을 pro-rata로 정산한다. 회차별 금액을 저장할 것도 없고 분기마다 같은 금액이라는 가정도 없다. 07-redemption → 정산 내부 동작 참조.
- ⚠️ 더 이상 선택값이 아니다. MVP 판단은 "투자자 화면이 그 숫자를 필요로 하지 않는 한 미룬다"였다. 운영이 텀시트에서 회차 수를 알고 있다는 이유였고, 그 숫자가 장식일 때는 맞는 말이었다. 그런데 배포가 유한한 날짜 목록을 써야 하게 되면서(v3-133) 그 개수를 말해 주는 것이 이 값이 됐고, 리마인더와 회차별 엔드포인트와 확인 카드가 전부 이 값을 읽는다. 이 조합에서는 필수다. 여전히 생성 시에만 설정 가능하므로 늦게 정할 수는 있어도 사후에 끼워 넣을 수는 없다.
epoch_date_basis(CALENDAR / FIXED_DAYS)와 epoch_roll_day(1~28)는 생성 시 받아 두는 답이다. 배포 후 확인 카드가 실제 만기를 기준으로 같은 목록을 다시 만들 수 있게 보관하며, 그래야 같은 걸 두 번 묻지 않는다. roll day가 28에서 멈추는 이유는 2월이 그렇기 때문이다. 29~31일이 없는 달을 메우는 모든 규칙은 파트너가 합의한 지급일을 옮기게 된다.
🔴 이 둘이 만들어 낸 날짜로 무엇을 해도 되는지는 v3-136에 있다. 사람에게 보여 주는 것은 되고, 체인에 쓰거나 스케줄을 결정하게 하는 것은 안 된다. 읽는 것 자체는 괜찮다. 위저드 미리보기, 확인 diff, 리마인더의 제안이 전부 그렇게 한다. 금지되는 것은 기계가 그 답에 따라 행동하는 것이다.
NAV 안전 한계 (nav_deviation_cap_bps / nav_staleness_seconds): v3-32
오프체인만이 아니라 불변 코어 안(updateNAV)에서 강제한다. 갱신당 편차 상한(예: ±5% = 500 bps), staleness 창, 서킷 브레이커다. 이들은 회차 정산도 게이팅한다. NAV가 상한을 넘어 움직였거나, 오래됐거나, 서킷 브레이커가 걸리면 executeEpoch가 자동으로 보류한다(NAV 계열의 이상치 보류). 어드민 setter는 setNavDeviationCap / setNavStaleness / tripCircuitBreaker / resetCircuitBreaker다.
변경 가능성 매트릭스: 무엇이 언제 바뀌나
운영자가 실제로 묻는 질문에 답하려고 모든 차원을 한곳에 모았다. 풀이 ACTIVE가 된 뒤에도 바뀔 수 있는 것은 무엇이고, 어떤 메커니즘을 거치는가? 두 번 읽어 둘 만한 미묘한 점은, 여러 필드가 DRAFT에서는 자유롭게 수정되다가 배포가 아니라 DRAFT → ACTIVE 전이 시점에 잠긴다는 것이다.
범례: 🔒 잠김 · 🟡 7일 타임락 + 멀티시그 · 🟢 즉시 · 🔴 생성 시에만 · ⚙️ 스케줄러 + 어드민 트리거.
| 필드 | DRAFT | ACTIVE 이후 | 이유 |
|---|---|---|---|
lockup_days | 🟢 자유 수정 | 🔒 잠김 | 투자자는 공시된 락업을 보고 입금했다. 입금 후 바꾸면 그 조건이 깨진다 |
maturity_days | 🟢 자유 수정 | 🔒 잠김 | 같은 이유다. 만기는 공시된 투자자 조건의 일부다 |
penalty_type | 🟢 자유 수정 | 🔒 잠김 | 조기 이탈 패널티는 입금 시점의 계약 조건이다 |
fx_rate_source | 🟢 자유 수정 | 🔒 잠김 | 풀이 라이브가 되면 NAV 환산 소스를 고정한다 |
chain_id | 🔒 불변 | 🔒 잠김 | 컨트랙트가 그 체인에 있다(배포 시 고정) |
accepted_currencies | 🟢 자유 수정 | 🔒 첫 입금 이후 잠김 | 입금 이력에 묶인다 |
operating_currency | 🔒 불변 | 🔒 잠김 | 기초자산 표시 통화다 |
is_showcase | 🔒 불변 | 🔒 잠김 | 풀의 근본 성격을 바꾼다 |
maturity_model | 🔒 불변 | 🔒 잠김 | 기초 컨트랙트 구조다 |
tranche_group_id / tranche_role | 🔒 불변 | 🔒 잠김 | 트랜치 연결은 배포 시 고정된다(v3-14) |
fund_wallet | 🟢 자유 수정 | 🟡 타임락 | 중대 사항이다. 파트너 잔여분의 목적지이므로 7일 타임락 + 멀티시그 |
treasury(Aset 수수료 지갑) | 🟢 생성 시 설정 | 🟢 즉시 · 멀티시그(타임락 없음) | 수수료 목적지(Aset)다. 수수료는 보류 없이 자동 분배되므로 타임락은 낡은 주소에 수수료를 묶어 두기만 한다. 멀티시그로 즉시 변경한다(v3-69) |
fund_fee_wallet(Pool mgmt 수수료 지갑) | 🟢 생성 시 설정 | 🟢 즉시 · 멀티시그(타임락 없음) | 수수료 목적지(펀드)이고 fund_wallet과 별개다. 같은 이유로 수수료 지급에 보류가 없어 타임락이 없다(v3-69) |
reserve_bps | 🟢 자유 수정 | 🟡 타임락 | 중대 사항이다. 투자자 보호 수준이므로 7일 타임락 + 멀티시그 |
jurisdiction_whitelist / enforce_jurisdiction / allows_us_persons | 🟢 자유 수정 | 🟡 타임락 | 중대 사항이다. 누가 투자할 수 있는지(관할)를 정하므로 7일 타임락 + 멀티시그 |
redemption_gating_bps | 🟢 자유 수정 | 🟡 타임락 | ⚠️ 폐기됨. MVP에 포함되지 않는다(2026-08-27, v3-150). 중대 사항인 유동성 규칙이므로 7일 타임락 + 멀티시그 |
yield_frequency | 🟢 자유 수정 | 🟢 자유 수정 | 운영 주기다(수동 트리거) |
net_yield_fee_config | 🟢 자유 수정 | 🟢 자유 수정 | 수수료 구조다 |
collateral_description / collateral_ratio | 🟢 자유 수정 | 🟢 자유 수정 | 계약 조건이 아니라 자산 설명 정보다 |
allow_rollover | 🟢 자유 수정 | 🟢 자유 수정 | 재투자 토글이다. ⚠️ MVP에서는 어느 화면에도 노출하지 않는다(2026-08-27, 아직 반영 안 됨, v3-151). 어드민 폼에서 토글이 빠지고 필드는 남는다 |
is_paused / is_emergency_frozen | 🟢 자유 수정 | 🟢 자유 수정 | 긴급 제어다. 설계상 즉시여야 한다 |
capacity | 🟢 자유 수정 | 🟢 올리는 것만 가능 | 오프체인 값이고 Lambda가 강제한다. 상한을 올리면 투자자를 더 받을 뿐 기존 홀더에게 해가 없다. 낮추는 것은 거부된다(v3-43) |
min_investment | 🟢 자유 수정 | 🔒 잠김 | 오프체인이지만 공시된 투자자 조건이다. 조건 무결성을 위해 ACTIVE 이후 잠근다. 바꾸려면 새 풀을 만든다(v3-43) |
lifecycle_status | — | ⚙️ 스케줄러 + 어드민 트리거 | 날짜 기반 전이다. 어드민이 CLOSED(및 reopen, v3-110) / IMPAIRED / WIND_DOWN을 트리거할 수 있다. 아카이브는 별개 축이고 여기의 값이 아니다 |
is_hidden | 🟢 자유 수정 | 🟢 자유 수정 | 노출 여부만 바꾸고 되돌릴 수 있으며 온체인 효과도 기능 변화도 없다. 구현된 것은 어드민 목록 필터뿐이다(v3-110 A · is_hidden) |
deleted_at(아카이브) | 🟢 아카이브·복원 | 🟡 조건부 | 종료에 가까운 lifecycle이고 모든 포지션이 0일 때만 아카이브할 수 있다. 온체인 pause()를 반영한다. 복원에는 사유가 필요하고 감사 대상이다(v3-110 A) |
redemption_type | 🟢 생성 시 설정 | 🔴 생성 시에만 | 온체인 RedemptionConfig에 있고 initialize에서만 설정되며 setter가 없다. 풀은 만들어질 때의 값에 고정된다(마이그레이션 0106) |
redemption_term_epochs · epoch_date_basis · epoch_roll_day | 🔴 생성 시에만 | 🔴 생성 시에만 | 구현 완료(0189, v3-132), dev 적용 대기. 본질적으로 생성 시 전용이다. 이 값은 투자자가 보고 입금한 스케줄의 끝을 정하고, 두 규칙 컬럼은 이미 기록된 목록을 계속 설명할 수 있어야 한다 |
epoch_duration_days | 🟢 생성 시 설정 | 🔴 생성 시에만 | 이미 입금한 투자자에게 상환 모델을 바꾸는 것은 공시되지 않은 상환 게이트를 거는 셈이다. 온체인 setter는 totalLPSupply > 0이 되면 revert하고, 타임락 거버넌스 경로는 v2로 미뤘다(v3-38) |
nav_deviation_cap_bps / nav_staleness_seconds | 🟢 자유 수정 | 🟡 타임락 | 불변 코어 안에서 강제되는 NAV 안전 한계다(v3-32) |
표의 메커니즘 열에서 오해하기 쉬운 두 가지:
- 수수료 목적지는 즉시 변경되며 타임락이 없다. (v3-69)
treasury와fund_fee_wallet은 멀티시그이지만 즉시 반영된다. 수수료 분배에는 보류가 없어서, 지연을 걸어도 그 기간에 잘못된 주소로 나가는 지급을 막지 못하고 수정만 늦출 뿐이다.fund_wallet(투자자 자본 잔여분)은 7일 타임락을 유지한다. 그래서 둘이 다른 행에 있다. - ACTIVE 이후의 🔒는 "새 풀을 배포하라"는 뜻이다.
lockup_days,maturity_days,penalty_type,fx_rate_source,min_investment에는 잠금 해제 경로가 없다.
검증 규칙
풀 배포나 수정 시 거부되는 잘못된 차원 조합이다.
| 규칙 | 이유 |
|---|---|
is_showcase = true인데 fund_wallet이 설정됨 | showcase 풀은 배포되지 않으므로 자금 유입 주체를 지정할 대상이 없다 |
is_showcase = true인데 입금·상환 설정이 있음 | 이 풀들은 거래를 처리하지 않는다 |
maturity_model = FIXED_TERM인데 maturity_days = NULL | FIXED_TERM은 만기일이 필요하다 |
maturity_model = OPEN_ENDED인데 maturity_days가 설정됨 | OPEN_ENDED(revolving)에는 고정 만기가 없다 |
penalty_type = FLAT_FEE인데 penalty_fee_amount = NULL | 고정 수수료에는 금액이 필요하다 |
penalty_type IN (PRINCIPAL_BASED, YIELD_BASED)인데 penalty_rate_bps = NULL | 이 타입들은 요율이 필요하다 |
tranche_group_id가 설정됐는데 같은 그룹 제약 위반 | 같은 그룹의 풀은 fund_wallet, operating_currency, chain_id, external_provider, maturity_days를 공유해야 한다(v3-14) |
tranche_group_id가 설정됐는데 그룹에 풀이 3개 초과 | 그룹당 최대 3개 트랜치(SENIOR/MEZZANINE/JUNIOR) |
tranche_role은 설정됐는데 tranche_group_id가 NULL(또는 그 반대) | 두 필드는 항상 함께 있어야 한다 |
fund_wallet이 ERC20을 못 받는 컨트랙트 | 자금이 묶인다. 배포를 실패시킨다 |
accepted_currencies가 빈 배열 | 풀은 최소 하나의 스테이블코인을 받아야 한다 |
operating_currency != 'USD'인데 fx_rate_source가 미설정 | 비USD 풀은 환율 처리가 필요하다 |
배포 스크립트와 어드민 UI 양쪽에서 강제한다. 백엔드 검증은 pools.post.create.ts 참조.
자금은 어떻게 흐르나
이 절은 돈이 풀을 통해 어떻게 움직이는지를 다룬다. 입금에서 fund wallet까지, reserve 처리, 스테이블코인 관련 세부사항이다.
처음부터 끝까지 보는 정본
세 스트림(자본 / 수수료 / Aset 수수료)을 아우르는 통합 자금 지도, 정본 지갑·은행계좌 네이밍(v3-70), 온체인과 오프플랫폼 경계는 **23-money-path**에 있다. 아래 내용은 그 그림으로 들어가는 재료다.
자금은 어디에 있나
단계별 보관 주체는 핵심 원칙 → 비수탁에 한 번 정리해 뒀다. Aset의 온체인 역할은 특정 함수를 호출하는 것(상환 승인, 수익 정산)뿐이고, 혼자서 자금을 옮길 수 없다.
Reserve 분리 로직
투자자가 입금하면 풀은 일부(reserve_bps, 기본 1000 = 10%)를 유동성 버퍼로 남기고 나머지를 파트너의 fund_wallet으로 보낸다.
외부 리저브 구현에서는 입금의 reserveBps 몫을 reserveWallet으로 보내고 나머지를 fundWallet으로 보낸다. 풀 내부 리저브 잔고를 적립하지 않는다. 아래 풀 내부 보유 예시는 구형 구현의 기록이다.
입금 시:
reserve_amount = deposit_amount × reserve_bps / 10000
partner_amount = deposit_amount - reserve_amount
Pool 컨트랙트가 reserve_amount를 보유(reserveBalance에 가산)
safeTransfer(fund_wallet, partner_amount) — 잔여분을 내보내고 ReleasedToPartner 이벤트 발생예시: $10,000 입금, reserve 10%
reserve_amount = $10,000 × 10/100 = $1,000 → Pool reserve에 남음
partner_amount = $10,000 - $1,000 = $9,000 → fund_wallet으로 전송
LP는 (deposit_amount / nav_per_token)만큼 투자자에게 민팅시점
분리는 항상 입금과 같은 트랜잭션에서 일어난다. "대부분의 풀에서"도 아니고 나중에도 아니다. LP 민팅과 잔여분 전송이 둘 다 deposit() 안에 인라인돼 있어서, 투자자 자본은 지갑을 떠난 그 블록에 fund_wallet에 도착한다. 분기는 없다. 예전에 hold-back 레버가 분기를 만들었는데 0183에서 제거됐다(23-money-path §3a).
스테이블코인(풀별, 체인별)
각 풀은 accepted_currencies에 정의된 스테이블코인을 하나 이상 받는다. 심볼에서 컨트랙트 주소로의 매핑은 체인에 따라 달라진다.
현재 지원하는 스테이블코인:
| 심볼 | 소수 자릿수 | 비고 |
|---|---|---|
| USDC | 6 | 기본값. Circle이 직접 배포한 USDC를 선호한다. |
| USDT | 6 | Tether. 대부분의 체인에 있다. |
| DAI | 18 | MakerDAO. 소수 자릿수가 18이다(USDC/USDT는 6). |
체인별 주소는 다음으로 해석한다.
STABLECOIN_ADDRESSES[chainId][symbol]예시:
- Base(chainId 8453)의 USDC:
0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 - Ethereum(chainId 1)의 USDC:
0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 - Kaia(chainId 8217)의 USDC: 미정
발행사가 직접 배포한 네이티브 스테이블코인만
브리지 버전(예: Polygon의 USDC.e)은 디페그 위험 때문에 제외한다. 발행사가 네이티브로 배포한 체인에서 Circle 발행 USDC, Tether 발행 USDT 등만 받는다.
입금 플로우 다이어그램
같은 흐름을 8단계로, 그리고 9단계가 없는 이유
단계:
- 투자자가 Pool이 자기 USDC를 쓸 수 있도록 approve한다
- 투자자가
Pool.deposit(USDC, amount)를 호출한다 - Pool이
safeTransferFrom으로 USDC를 가져온다 - Pool이 같은 트랜잭션 안에서 즉시 LP 토큰을 투자자에게 민팅한다
- Pool이 reserve 비율만큼을
reserveBalance로 보유한다 - Pool이 파트너 잔여분을
fund_wallet으로safeTransfer하고ReleasedToPartner를 발생시킨다.deposit()끝부분에 인라인돼 있고, 따로 호출할releaseToPartner()함수는 없다(23-money-path §3a) - Pool이
Deposited이벤트를 발생시킨다 - Aset이 입금을 기록한다.
POST /deposits가 영수증에서Deposited이벤트를 검증하고, 온체인 인덱서가 약 2분 주기로 정합을 맞춘다
9단계는 없다. 흐름은 Aset 자신의 기록에서 끝난다. 입금 시 호출하는 파트너 엔드포인트가 없고, FM은 대신 플랫폼을 읽는다(v3-115에 전체 정정이 있다). 참고로 ReleasedToPartner는 인덱싱되지 않으므로 릴리스 구간에는 오프체인 기록이 전혀 없다(23-money-path §3f).
수익은 어떻게 동작하나
수익은 기초 펀드에서 발생하고(예: 차주가 대출을 상환) 투자자에게 돌아온다. 메커니즘은 yield_frequency, 트랜치 그룹 소속 여부(v3-14), net_yield_fee_config에 달려 있다. (분배는 claim 기반 수동 트리거뿐이고 AUTO는 v3-20에서 제거됐다.)
수익 흐름 다이어그램
Net Yield 계산
net_yield_fee_config가 있는 풀은 총 수익을 투자자 몫과 수수료 갈래로 나눈다. 계산은 Lambda에서 오프체인으로 한다. 수수료 조건이 파트너마다 다르고, DB 설정 변경이 컨트랙트 업그레이드보다 낫기 때문이다.
총 수익 $1,000에 Joob 설정 {platform_yield_take_bps: 100, perf_fee_bps: 2000, perf_hurdle_bps: 1500}을 적용한 예시:
admin_fee = gross × platform_yield_take_bps / 10000 = $1,000 × 100/10000 = $10
# 성과 수수료는 hurdle(15% APY = 1500 bps)를 넘을 때만 적용
perf_fee = (pool_apy × 100 > perf_hurdle_bps)
? gross × perf_fee_bps / 10000 = $1,000 × 2000/10000 = $200
: 0
net = gross − admin_fee − perf_fee = $790
Pool.settleYield(usdc, 790, 210, 0) # 한 tx: 790 → LP 홀더, 210 → 트레저리수수료 목적지
수수료는 수령 주체에 따라 목적지가 둘이다(v3-69).
| 수수료 | 목적지 | 수령 주체 |
|---|---|---|
| 플랫폼 수익 몫 · SPC mgmt · 성과 수수료(휴면) | treasury_wallet. Aset의 멀티시그(예: Gnosis Safe) | Aset |
| Pool mgmt | fund_fee_wallet. 풀별로 두며 입금 money-path의 fund_wallet과 별개다 | 펀드매니저 |
두 갈래 모두, 홀더에게 net을 크레딧하는 settleYield(stablecoin, netAmount, treasuryAmount, poolMgmtAmount) 호출 한 번 안에서 지급된다. net 크레딧과 수수료 합계를 unclaimedYield에 대해 한 번 검사하고, 0이 아닌 갈래마다 전송하며 각자 FeesWithdrawn을 발생시킨다. fund_fee_wallet이 설정되지 않은 상태의 pool mgmt 갈래는 address(0)으로 태우지 않고 revert한다(FundFeeWalletNotSet). 금액은 Lambda에서 오프체인으로 계산한다. 컨트랙트는 수수료 요율을 저장하지 않는다. 입금 money-path(90/10, 불변)는 건드리지 않는다. 이건 분배 단계에만 해당한다.
두 목적지 지갑(treasury_wallet, fund_fee_wallet)은 타임락 없는 즉시 멀티시그 setter다. fund_wallet과 다른 이유는 변경 가능성 매트릭스 참조.
트랜치 그룹 워터폴 (v3-14)
풀들이 tranche_group_id를 공유하면 수익과 손실이 연결된 풀들에 걸쳐 워터폴을 따른다. 각 풀은 자기 NAV를 가진 표준 SINGLE 풀이고, 워터폴은 NAV 계산 시 Aset 오라클 Lambda가 적용한다.
스마트 컨트랙트 영향은 없다. 풀 하나에 LP 둘도 아니고 온체인 워터폴 코드도 없다. Lambda가 각 풀의 NAV를 계산해 풀마다 따로 Pool.updateNAV()를 호출한다. v3-15가 Aset을 유일한 NAV 처리 주체로 만들었기 때문에 가능하다.
오프체인으로 하는 대가는 신뢰 모델이다. Senior 투자자는 Aset이 Junior를 먼저 상각할 것이라고 믿어야 한다. v1에서는 이를 받아들인다. NAV 가드(v3-32)와 money-path 불변성이 경계를 잡아 주므로 최악의 경우가 절도가 아니라 잘못된 상각이다. 무신뢰 온체인 워터폴은 나중에 별도 풀 타입으로 추가할 수 있다(v3-39).
수익 워터폴 (Senior가 먼저 받는다)
⚠️ 설계만 있고 구현되지 않았다. v3-50은 손실 워터폴을 먼저 구현했다. 손실 보호가 트랜치의 핵심이기 때문이다. 수익은 현재 풀별로 독립 분배되고, Senior APY 우선 배분은 딜이 요구할 때 추가한다(v3-50).
의도한 설계는 이렇다. Lambda가 Senior 풀에 목표 요율(apy_rate × LP 공급량 × 기간)을 먼저 지급하고, Mezzanine이 있으면 그다음, Junior가 나머지를 가져간다. 그래서 Junior는 좋은 기간에 더 벌고 나쁜 기간에는 아무것도 못 번다. 각 풀의 NAV가 배분된 몫으로 갱신되고 투자자는 자기 풀의 그 NAV로 상환한다.
손실 워터폴 (Junior가 먼저 흡수한다)
✅ 구현 완료(v3-50). POST /tranche-writedown이 엔진(lib/shared/tranche/loss-waterfall.ts)을 Junior → Mezzanine → Senior 순으로 돌리고, 각 풀의 새 NAV를 풀별 NAV 경로로 기록한다. 스마트 컨트랙트 변경은 없다. NAV 쓰기 자체의 동작(nav_proposals 행 없음, 하락 시 풀별 24시간 타임락, 풀 간 atomic 보장 없음)은 v3-106에 기록돼 있다.
거버넌스 규칙이 셋 적용되고 전부 머지된 코드다(v3-106).
- ADMIN / SUPER_ADMIN만 가능하다.
OPERATOR호출은 403이다. 그룹 전체를 상각하는 것이 세 NAV 쓰기 경로 중 가장 큰 조작이기 때문이다. - Junior가 전부 소진되면 온체인으로 에스컬레이션한다.
proposeImpairmentOnChain을 호출하고impairment_proposed_at과junior_depleted_at을 기록하며lifecycle_status는 건드리지 않는다. 라벨은 7일 타임락 뒤executeImpairment에서 바뀐다. 그 기간에도 입금은 열려 있으므로(자동으로 pause되는 것은 없다, v3-94) 응답에deposits_open_during_timelock을 실어 운영자가 의도적으로 pause하도록 한다. - 완전 소진만 IMPAIRED다. 일부만 맞은 Senior나 Mezzanine은 NAV < 1.0인 채
ACTIVE로 남는다. IMPAIRED Junior 옆에 ACTIVE Senior가 있는 것은 표류가 아니라 올바른 정상 상태다.
손실(v3-13의 DPD 상각이든 다른 무엇이든)은 Junior NAV를 먼저 치고, Junior가 소진돼야 Mezzanine, 그다음이 Senior다. Junior 자본이 $300인데 손실이 $500이면 Junior가 $300을 흡수하고 소진되며, Senior의 NAV가 남은 $200만큼 떨어지되 상태는 ACTIVE로 남는다.
소진된 Junior는 IMPAIRED로 올라간다. 입금은 멈추고, 상환은 락업과 패널티가 면제된 채 열려 있으며, 파트너가 자본을 넣으면 회복할 수 있다(v3-12).
⚠️ "NAV 0"은 실제로 저장되지 않는다. 컨트랙트가 newNav == 0을 거부하고 DB에 CHECK (nav_per_token > 0)이 있어서, 소진된 트랜치는 0.000001로 기록된다(NAV_FLOOR, tranche.post.writedown.ts의 로컬 상수). "사실상 0"이라고 말하는 것은 숫자가 아니라 lifecycle 전이다.
트랜치 그룹 파라미터
워터폴은 각 풀의 표준 필드를 쓴다. 트랜치 전용 컬럼은 없다.
- 각 풀의
apy_rate가 수익 목표를 정한다(Senior는 낮게, Junior는 높게) - 각 풀의
capital(=lp_supply × nav)이 손실 흡수 능력이다 - 손실 순서는 오직
tranche_role로 정해진다(Junior → Mezzanine → Senior)
그룹 안의 각 풀은 자기 reserve를 유지하고, 그 reserve가 자기 상환을 먼저 지급한다. reserve와 트랜치는 서로 다른 메커니즘이다. 핵심 원칙 → Reserve와 Tranche는 다르다 참조.
상환은 어떻게 동작하나
표준 상환 메커니즘(락업 확인, 패널티 계산, NAV 스냅샷, 지급)은 상환에 자세히 있다. 이 절은 파트너가 응답하지 않을 때 쓰는 긴급 wind-down 경로를 다룬다.
긴급 Wind-Down
구형 청산 산식 이력
아래의 풀 잔고 기반 산식과 다이어그램은 구형 구현을 설명한다. 현재 외부 리저브 구현의 executeWindDown()은 오라클 NAV를 유지한다. 외부 지갑의 자금 투입과 실제 지급은 별도다.
파트너가 응답하지 않게 되면(시스템 장애, 지급불능, 장기 무활동 등) 풀을 청산할 수 있다. 풀을 종료하고 회수 가능한 것을 투자자에게 배분한다.
파트너 잔여분 회수는 보장되지 않는다
현재 소스의 executeWindDown()은 navPerToken을 재계산하지 않는다. 오라클 NAV를 유지하며 실제 지급에는 별도 자금 투입과 상환 절차가 필요하다.
fund_wallet에 있는 잔여분은 파트너가 통제하고 온체인으로는 회수할 수 없다. 오프체인 법적 절차가 필요하다. 파트너 운영 풀에 내재한 리스크이고, 입금 시점에 투자자에게 반드시 알려야 한다.
Wind-Down 절차 (3단계, 약 90일)
같은 90일을 날짜별 타임라인으로, 각 단계의 정확한 상태 변화까지
T+0 파트너와의 마지막 연락
T+60일 Aset Lambda가 감시하다가 "60일 무응답"으로 표시
T+60일 어드민(멀티시그)이 Pool.proposeWindDown() 호출
→ WindDownProposed 이벤트 발생
→ 투자자와 파트너에게 통지
→ 30일 타임락 시작
T+60~90 타임락 기간:
- 파트너가 응답하면 멀티시그가 cancelWindDown()으로 중단
- Aset이 webhook, 이메일, 오프체인 채널로 접촉 시도
- LP 홀더는 풀 상세에서 "wind-down 대기" 경고를 본다
T+90일 타임락 경과 → 누구나 Pool.executeWindDown() 호출 가능
→ navPerToken이 아래 pro-rata 산식으로 강제됨(v3-12)
→ lifecycle_status = WIND_DOWN (whenActive 게이트로 입금 차단)
→ isEmergencyFrozen은 그대로(동결하면 출구가 막히는데, wind-down에 필요한 것과 정반대)
→ 락업과 패널티 면제
이후 각 LP 홀더가 상환 // 표준 requestRedemption → approveRedemption (v3-12)
→ payout = lpAmount × navPerToken (= reserve의 pro-rata 지분)
→ LP 소각, USDC 전송
→ 이미 릴리스된 잔여분은 잃는다(파트너 지갑에 있고 온체인으로 회수 불가)redeem()은 줄임말이고 그런 함수는 없다. WIND_DOWN에서 상환은 락업과 패널티가 면제된 표준 requestRedemption → claim 경로로 돈다(v3-12가 한때 제안됐던 독립 claimWindDown()을 없앴다). navPerToken은 다음으로 강제된다.
navPerToken = (reserveBalance + 미소진 회차 추가분)
÷ (totalSupply − settledUnclaimedLp)그래야 모든 홀더의 pro-rata 지급액 합이 분자와 정확히 같아진다. 유도 과정과 양쪽을 함께 바꿔야 했던 이유는 06 → R10에 있다.
⚠️ 요청 게이트가 분자와 같은 버킷을 잰다. 그래서 이 상태에서는 PENDING_RESERVE나 파트너 top-up이 생길 수 없고, 그게 요점이다. 침묵으로 wind-down을 촉발한 파트너가 자금을 넣을 리 없기 때문이다. 둘이 어긋났을 때는 청산된 풀이 가격을 매겨 놓고 정산은 거부하는 일이 벌어졌다. test_WindDown_HeldFundsAreReachable_NoPendingReserve가 이를 고정한다.
왜 60일 + 30일인가
근거와 따르고 있는 업계 관행
전통 금융의 관행을 따랐다.
- 무응답 60일: "파트너가 바쁜 것"과 "파트너가 사라진 것"을 구분하기에 충분한 시간이다. Aset이 여러 채널로 확인한다.
- 타임락 30일: 파트너에게 마지막 응답 기회를 준다(점검 중이었거나 규제 이슈가 있었을 수 있다).
- 합계 약 90일: 사모 크레딧 업계의 디폴트 기준("90일 연체" = 공식 디폴트 임계)과 맞다.
출처: Maple Finance 디폴트 정책, 사모 크레딧 standstill 조항, ABS 디폴트 프레임워크.
Wind-Down 다이어그램
is_showcase = false인 모든 풀은 상세 페이지에 파트너 리스크와 wind-down 회수 한계 안내를 함께 싣는다. 정확한 문구와 발동 조건은 아래 리스크 고지 참조.
리스크 고지
투자자는 각 풀의 상세 페이지에서 표준 리스크 안내를 본다. 안내는 풀의 차원 값에 따라 자동으로 생성된다.
표준 안내(자동 생성)
각 풀에 대해 UI가 해당되는 리스크 안내를 접이식 상자로 보여 준다.
환위험 안내는 의도적으로 없다. operating_currency 참조.
| 리스크 | 발동 조건 | 표시 문구 |
|---|---|---|
| 파트너 리스크 | is_showcase = false이고 fund_wallet ≠ Aset 트레저리 | "예치금 대부분은 펀드 운용사가 보유합니다. 운용사에 문제가 생기면 온체인으로 회수 가능한 것은 Pool reserve(X%)뿐입니다." |
| 락업 리스크 | lockup_days > 0 | "자금은 입금일로부터 [N]일간 묶입니다. 조기 상환에는 패널티가 붙습니다: [패널티 상세]." |
| 트랜치 리스크(Senior) | tranche_role = SENIOR(v3-14) | "Senior LP를 보유하고 있습니다. Junior/Mezzanine 풀이 손실을 먼저 흡수하지만, 극단적인 경우 그들이 소진되면 손실이 발생할 수 있습니다." |
| 트랜치 리스크(Junior) | tranche_role = JUNIOR(v3-14) | "Junior LP를 보유하고 있습니다. 손실을 먼저 흡수하는 대신 좋은 기간에는 더 높은 수익을 받습니다. 손실이 Junior 자본을 넘으면 투자금 전액을 잃을 수 있습니다." |
| 담보 리스크 | collateral_ratio < 100이거나 collateral_description = NULL | "이 풀의 기초 대출은 [부분 담보 / 무담보]입니다. 차주가 상환하지 못하면 손실 위험이 더 큽니다." |
| NAV 상각 리스크 | 모든 풀 | "기초 펀드의 가치가 하락하면(예: 대출 부실) NAV가 낮아지고 토큰 가치도 그만큼 떨어집니다. 운용사의 equity 버퍼와 (있다면) Junior 트랜치가 손실을 먼저 흡수합니다."(풀 reserve는 흡수하지 않는다. R8) |
| Wind-down 회수 한계 | is_showcase = false | "wind-down이 일어나면(파트너 60일 이상 무응답) 온체인으로 회수 가능한 것은 Pool reserve(X%)뿐입니다. 나머지는 오프체인 법적 절차가 필요합니다." |
| 미러 커스터디 | custody_mode = 'MIRROR' | "이 풀은 외부 컨트랙트에서 추적한 포지션을 보여 줍니다. Aset은 수탁하지 않으며 기초자산에 대해 어떤 보증도 하지 않습니다." |
표시 위치
- 풀 상세 페이지: 해당되는 안내를 "Risks" 절에 접이식으로 전부 표시
- 입금 플로우: 트랜잭션 서명 전 확인 모달에서 핵심 리스크를 다시 표시
- 풀 카드(목록): 조합에 따라 산출된 리스크 배지 하나(Low/Medium/High)
리스크 등급(계산값) ✅ Decided 2026-06-16
풀 카드(목록)와 상세에 표시되는 Low / Medium / High 배지를 자동 계산한다. 어드민이 고르는 값이 아니다. 렌더 시점에 풀 상태에서 파생된다. 폐기된 collateral_type 배지(v3-07)를 대체한다. SoT는 Decision Log의 Pool Card Risk Tier.
담보 비율이 아니라 채권 중심으로 등급을 매기는 이유
담보 비율이 아니라 채권 중심이다. 우리 풀은 사모 크레딧과 채권(par 100% 안팎)이다. Maple, Goldfinch, Centrifuge처럼 담보 비율이 아니라 구조(트랜치)와 포트폴리오 건전성으로 등급을 매긴다. 그래서
collateral_ratio가 주된 요인이 아니고, FX와operating_currency는 제외한다(운용사가 관리한다). NPL과 NAV는 별개 신호다. NAV는 실현된 손실(상각된 원금)을 반영하고, NPL은 아직 NAV에 들어오지 않은 선행·미실현 연체 신호다. 그래서npl_ratio가 조기 경보로 배지에 직접 들어간다. (누적 상각 손실은 표시 전용이다. 이미 NAV에 반영돼 있어서 별도 배지 입력이 아니다.)
우선순위: High → Medium → Low(먼저 맞는 것이 이긴다):
| 등급 | 조건(하나라도 해당) |
|---|---|
| 🔴 High | lifecycle_status ∈ {IMPAIRED, WIND_DOWN} · is_emergency_frozen · tranche_role = JUNIOR · nav_per_token < 0.90(실현된 상각) · NPL > 5%(Equity Buffer 한도 초과) |
| 🟡 Medium | nav_per_token 0.90~0.999(경미한 상각) · tranche_role = SENIOR · NPL 2~5%(버퍼가 흡수 중인 선행 신호) 또는 DPD30/60 상승 추세 · reserve_bps < 1000 |
| 🟢 Low | 위에 해당 없음(collateral_ratio ≥ 100 / nav ≥ 1.0 / 트랜치 없음 / lifecycle 정상) |
임계값(2026-06-16 확정): NPL <2 / 2~5 / >5% · reserve 기준선 10% · NAV <0.90 / 0.90~0.999(NAV가 낮을수록 나쁘다, 1.0이 정상).
어떤 신호가 라이브이고 어떤 것이 배선 중인지, 배지는 얼마나 자주 다시 계산되는지
단계와 데이터 출처:
- 1단계(라이브):
lifecycle_status+is_emergency_frozen+nav_per_token+reserve_bps+tranche_role(Junior는 High, Senior와 Mezzanine은 Medium. v3-50에서 배선됐다. 트랜치 없는 단독 풀은 Low로 남는다). - NPL%(S2, 배선 중):
DPD ≥ npl_threshold_days인 연체 노출 합계 ÷ total_outstanding_principal(파트너 보고. DPD 버킷과 스냅샷에서 나온다).> 5%면 High,2~5%면 Medium. NAV와 별개인 선행 신호다. 완만한 폴백: 펀드가 데이터를 주지 않아 분모가 없으면npl_ratio는 null이 되고 풀은 1단계 신호만으로 점수를 받는다. Joob API 2차 데이터 요청에 의존한다. - 3단계(보류): 집중도(top-N / HHI) · 평균 만기 · 활용률 · 섹터. 선택적 파트너 데이터이고 아직 쓰지 않는다. 외부 펀드 데이터 제공자가 없는 풀은
reserve_bps+lifecycle_status+nav_per_token+tranche_role만으로 점수를 매긴다.
갱신 주기: lifecycle, 동결, 중대한 NAV 상각은 이벤트 기반으로 즉시 반영한다. 포트폴리오 건전성(NPL / DPD)은 매월 다시 계산한다. 등급이 오르내리며 흔들리지 않도록 히스테리시스를 둔다(하향은 즉시, 회복은 연속 두 스냅샷 필요).
이것은 참고 정보일 뿐이다. 투자자는 전체 리스크 고지를 읽어야 한다.
참고 예시
차원 모델을 쓴 풀 설정 예시 넷이다.
예시 1: Aset 직접 풀 (⛔ 배포 불가)
외부 파트너 없이 Aset이 직접 운영하는 풀이다. 플랫폼이 거부하는 설정의 실례로 남겨 둔다. v3-26이 배제한 형태이고, 그 이유가 커스터디 논증 전체이기 때문이다.
⛔ 이 설정은 배포 시 revert된다
fund_wallet이 Aset 트레저리이면 모든 입금의 파트너 잔여분이 Aset으로 가고, 그러면 Aset이 수탁자가 된다. PoolConfigLib.validate는 fundWallet == treasuryWallet일 때 **FundWalletIsTreasury()**로 revert하므로(PoolConfigLib.sol:60) 이 풀은 만들어질 수 없다. 어드민 API로도, 그것을 우회하는 팩토리 직접 호출로도 안 된다. 그러므로 아래 YAML은 템플릿이 아니라 반례다. fund_wallet 차원 참조.
파트너 지갑이 없는 유일한 풀은 is_showcase = true 등재이고, 배포되지 않으므로 입금 플로우 자체가 없다. custody_mode = 'MIRROR' 풀은 fund_wallet을 갖는다(FJL이 그렇다).
YAML 설정: 거부되는 형태, 33줄
# Fund Flow & Custody
fund_wallet: "0xAsetTreasury..." # ⛔ == treasury_wallet → reverts FundWalletIsTreasury()
reserve_bps: 1000 # 10%
is_showcase: false
# Asset Configuration
chain_id: 8453 # Base mainnet
accepted_currencies: ['USDC']
operating_currency: 'USD'
fx_rate_source: FIXED # Not used since USD
collateral_description: "Diversified short-term loans"
collateral_ratio: 100
# Yield Configuration
yield_frequency: MONTHLY
net_yield_fee_config: NULL # No partner fees
# Investor Terms
lockup_days: 0 # independent of penalty_type (v3-83)
maturity_model: FIXED_TERM
maturity_days: 365
penalty_type: NO_EARLY
# (no tranche group — standalone pool)
allow_rollover: true
# Compliance (jurisdiction only — level gating removed in 0063)
enforce_jurisdiction: false # open to all jurisdictions
allows_us_persons: false
# Pool State (runtime)
lifecycle_status: ACTIVE
is_paused: false
is_emergency_frozen: false예시 2: Joob FJL1 (Aset을 통한 신규 투자자)
Aset을 거쳐 들어오는 신규 투자자를 위해 설정한 Joob FJL1 풀이다. 자금은 Henon의 Ethereum 지갑으로 가고, 파트너는 Joob/Henon이다.
YAML 설정: 36줄, 회차 상환 + IDR 운영 통화
# Fund Flow & Custody
fund_wallet: "0x24c5a8866f38b0659d31cbee821559ea22d1e736" # Henon Ethereum USDC
reserve_bps: 1000 # 10%
is_showcase: false
# Asset Configuration
chain_id: 1 # Ethereum mainnet
accepted_currencies: ['USDC']
operating_currency: 'IDR'
fx_rate_source: FIXED # 16,730 IDR/USD
collateral_description: "Indonesian EWA payroll receivables"
collateral_ratio: 100
# Yield Configuration
yield_frequency: QUARTERLY
net_yield_fee_config:
platform_yield_take_bps: 100
perf_fee_bps: 2000
perf_hurdle_bps: 1500
# Investor Terms
lockup_days: 90
maturity_model: FIXED_TERM
maturity_days: 365
penalty_type: YIELD_BASED # 50% dividend forfeiture
# (no tranche group — standalone pool)
allow_rollover: false
# Compliance (jurisdiction only — level gating removed in 0063)
enforce_jurisdiction: false # ShardLab/Hashed are institutions; no country restriction
allows_us_persons: false
# Pool State
lifecycle_status: ACTIVE
is_paused: false
is_emergency_frozen: false예시 3: Joob FJL1 (ShardLab/Hashed 레거시)
같은 풀이지만 ShardLab/Hashed의 기존 RPS 보유분을 표시만 하는 미러다. 이들은 Aset이 온보딩하기 전에 Joob과 직접 투자했고, Aset은 포지션을 보여 주기만 한다.
# Fund Flow & Custody
custody_mode: MIRROR # ← Aset은 관여하지 않고, 파트너가 보고한 것을 미러링만 한다
# Asset Configuration
chain_id: 8217 # Kaia (Joob의 RPS 컨트랙트가 있는 곳)
operating_currency: 'IDR'
fx_rate_source: FIXED
collateral_description: "Indonesian EWA payroll receivables"
collateral_ratio: 100
# 나머지 설정: custody_mode = MIRROR면 의미 없음Aset은 Kaia에서 RPS.balanceOf(wallet)으로 잔액을 읽어 표시한다. Aset 쪽에는 입금도 상환도 수익 처리도 없다.
예시 4: LINE BK PoC (계획 중)
LINE BK의 PoC 풀이다. 세부 사항은 LFC와의 최종 합의를 기다리는 중이다.
YAML 설정: 40줄, 대부분 미정
# Fund Flow & Custody
fund_wallet: TBD # LFC SG SPV (multi-sig)
reserve_bps: 1000 # 10%; may be higher (e.g., 1500-2000 bps / 15-20%) per LFC negotiation
is_showcase: false
# Asset Configuration
chain_id: TBD # likely Ethereum mainnet or Base
accepted_currencies: ['USDC']
operating_currency: 'THB' # or 'MYR' depending on originator
fx_rate_source: FIXED # or EXTERNAL_FEED if LFC provides daily rate
collateral_description: "Thai SME invoice receivables" # tentative
collateral_ratio: 100
# Yield Configuration
yield_frequency: MONTHLY # PoC requires monthly distribution (manual-trigger; AUTO removed v3-20)
net_yield_fee_config:
# TBD — likely management fee + performance fee
# platform_yield_take_bps: 150
# perf_fee_bps: 1500
# perf_hurdle_bps: 1000
# Investor Terms
lockup_days: 30 # tentative
maturity_model: OPEN_ENDED # "revolving": 3-6 month underlying loans, pool runs 6 months
maturity_days: NULL # not used in OPEN_ENDED
penalty_type: PRINCIPAL_BASED # tentative — ⚠️ INERT on OPEN_ENDED (no EARLY window, so no early-exit penalty ever applies). Effectively NO_EARLY; use FIXED_TERM if a penalty is actually intended.
tranche_group_id: <UUID> # linked Senior+Junior pools (v3-14)
tranche_role: SENIOR # or JUNIOR
allow_rollover: true
# Compliance (jurisdiction only — level gating removed in 0063;
# institution-only pools are no longer enforced on-chain — gate by jurisdiction)
enforce_jurisdiction: true
kyc_jurisdiction_whitelist: ['KOR']
allows_us_persons: false
# Pool State
lifecycle_status: DRAFT # being designed
is_paused: false
is_emergency_frozen: false⚠️ 위 값들은 2026년 5월 회의록에서 나온 작업 초안이다. LINE BK 설정은 아직 LFC와 확정 중이고 PoC 계약이 체결되면 갱신된다.
마이그레이션 노트
v2.x의 바이너리 pool_type 모델이 v3.0 차원에 어떻게 대응되는지, 무엇이 폐기됐는지다.
v2.x → v3.0 대응
v2.x pool_type | v3.0 대응 |
|---|---|
AS_POOL | ⛔ v3.0에 대응하는 것이 없다. fund_wallet = Aset 트레저리에 대응했는데, v3-26이 이를 없앴고 컨트랙트가 이제 거부한다(FundWalletIsTreasury()). 파트너가 없는 풀은 is_showcase = true로 표현한다 |
FUND_POOL | fund_wallet = 파트너 지갑 + 같은 유연한 설정 |
| (v2.x에 없음) | 읽기 전용 풀은 is_showcase = true |
| (v2.x에 없음) | tranche_group_id + tranche_role을 통한 트랜치 그룹(v3-14) |
폐기된 컬럼(v3.0에서 제거)
| 컬럼 | 대체 |
|---|---|
pool_type | 차원 조합(고정 타입 없음) |
lp_issuance_model | 제거됨. LP는 항상 Aset이 민팅 |
escrow_model | 제거됨. Escrow가 Pool로 흡수됨(10/90 분리가 deposit()에 인라인) |
signing_method(풀별 멀티시그 선택) | 제거됨(마이그레이션 0015). money-path가 업그레이드 불가이고 자금 목적지가 불변이라, 단일 cold key와 타임락만으로 비수탁이 성립한다. 풀별 멀티시그는 없다 |
폐기된 컨트랙트
PlatformReceiptNFT: 제거됨. LP 토큰 자체가 입금 증빙이라 별도 Receipt NFT가 필요 없다.PlatformEscrow: 제거됨.PlatformPool로 흡수됐다. Pool이deposit()안에서 reserve와 파트너 몫(10/90) 분리를 직접 처리하고, 파트너 몫을 전송하며ReleasedToPartner를 발생시킨다(별도 함수 없음). 별도 에스크로 컨트랙트는 없다. v3.0 아키텍처는 활성 컨트랙트 4개다.PlatformPool,PlatformLPToken,PlatformKYCSoulbound,PlatformPoolFactory. 스마트 컨트랙트 참조.
폐기된 동작
- D+7 자동 환불 창: 제거됨. 투자 플랫폼은 보통 환불을 제공하지 않는다. 파트너 지급불능은 wind-down으로 처리한다.
- 펀드매니저가 발행하는
FM_ISSUEDLP 토큰: 제거됨. LP는 항상 Aset이PlatformLPToken으로 민팅한다. - 상환 2단계 승인(
Operator → Admin): 제거됨. 어드민 단일 승인이고, reserve가 부족하면PENDING_RESERVE로 폴백한다.
마이그레이션 현황
v3.0은 구현됐고 Sepolia 테스트넷에 배포됐다(2026-06-04). 진행 상황은 다음과 같다.
- ✅ 스펙 작성(이 문서)
- ✅ DB 스키마 마이그레이션(신규 차원 컬럼 추가, 폐기 컬럼 표시). dev에 적용됨
- ✅ 스마트 컨트랙트(PlatformReceiptNFT와 PlatformEscrow 제거로 활성 4개, Pool 함수 추가). 테스트 183건 통과, Clones 팩토리 Sepolia 배포
- ✅ 백엔드 Lambda 갱신(차원 기반 분기, 수익 3단계, wind-down·impairment·거버넌스·freeze, 풀별 KYC 게이팅)
- ✅ 프론트엔드 데이터 계층 정렬(web과 admin-web 빌드 정상). 📋 신규 제어 UI(wind-down·거버넌스·freeze), 외부 감사, 메인넷 배포는 아직 대기
구현 진행은 Phase 1.5 계획에서 추적한다.
용어와 참고
용어
- 차원(Dimension): 풀의 개별 설정값 하나(예:
reserve_bps,fund_wallet). v3.0 모델은 고정 풀 타입 대신 약 24개 차원을 쓴다. fund_wallet: 각 입금의 파트너 잔여분이 가는 외부 지갑. 파트너가 통제해야만 한다. Aset 트레저리와 같으면 안 되고 컨트랙트가 init에서 이를 강제한다(v3-26).- Reserve: 외부 리저브 구현에서는 입금의
reserveBps몫을reserveWallet으로 보내고 나머지를fundWallet으로 보낸다. 풀 내부 리저브 잔고를 적립하지 않는다. - 트랜치 그룹(v3-14):
tranche_group_id를 공유하는 연결된 SINGLE 풀 2~3개. 각자 다른tranche_role(SENIOR/MEZZANINE/JUNIOR)을 갖고, Aset 오라클 Lambda가 이들에 걸쳐 손실 워터폴을 적용한다. 단독 풀은tranche_group_id가 NULL이다. - Junior 트랜치: 트랜치 그룹의 first-loss 풀. Mezzanine과 Senior보다 먼저 손실을 흡수한다. 잠재 수익이 더 높다.
- Senior 트랜치: 트랜치 그룹에서 손실로부터 보호되는 풀. 손실을 마지막에 흡수하고 수익이 낮다.
- NAV(순자산가치): LP 토큰 1개의 가치. Aset의 오라클 role이
updateNAV()로 갱신한다. - Wind-Down: 파트너가 응답하지 않을 때의 긴급 풀 종료(60+30일 타임락).
- 표시 전용 풀: 커스터디 없이 외부 포지션을 미러링하는 풀(예: 레거시 보유분).
PENDING_RESERVE: reserve가 부족해 어드민이 파트너와 조율하는 동안의 상환 상태.
문서 간 참조
- 투자 라이프사이클: 입금과 수익 분배 플로우
- Writedown & NAV: NAV 갱신 메커니즘, $1.00 상한, 24시간 하락 타임락
- 상환: 락업 메커니즘, 패널티 타입, 상환 플로우
- 스마트 컨트랙트: 컨트랙트 구조, 함수, 역할
- RBAC & 권한: 어드민 역할, 멀티시그 요건
- 상태 머신: lifecycle 전이, 상환 상태
- DB 스키마: 풀 테이블 컬럼, 폐기 필드
- 결정 로그: 특정 설계를 그렇게 정한 이유
- Joob 풀 설정: Joob FJL1 세부사항
내부 참조
- Phase 1.5 계획: 구현 로드맵
- 풀 차원 결정 로그(
Claude-Plan/Active/pool-dimensions-decisions-2026-05-29.md, 로컬 전용): 설계 근거와 결정 이력