Skip to content

커스터디 & 비수탁 보증

커스터디 정본. 비수탁 판정 기준, 키 모델, money-path 불변성, 업그레이드 거버넌스, 긴급 동결 시 이탈권을 한곳에 모았다. 04/08/09는 각자의 기능 맥락만 남기고 여기를 참조한다.

한 줄 원칙

커스터디는 **"자산을 우리가 보관하느냐"**가 아니라 **"운영자가 단독으로 유저 자금을 움직일 수 있느냐"**로 판정한다. Aset은 유저 자금을 단독으로 움직일 수 없다. 따라서 비수탁이다.

외부 리저브 이후 출구 조건

7일 무권한 청구 폴백 claimRedemptionFallback은 제거됐다. 회차 정산과 청구는 실제 투입된 자금에 의존한다. 운영 SLA 전환은 승인됐지만 응답 기한이나 지급 보장을 이 문서에서 만들지 않는다. 이는 별도로 남아 있는 회차 정산·대리 청구 기능을 제거한다는 뜻이 아니다.

근거: PoolLedgerLib.releaseReserveShare, GovernanceLib.executeWindDown, RedemptionLib노션 §1-4·§7-9·§8-2·§8-3. 수탁에 관한 대체 법무 문구는 별도 검토 대상이다.

1. 비수탁 판정 6기준

운영자가 컨트랙트 업그레이드·운영 권한을 가지고 있더라도 "운영자가 단독으로 유저 자금을 움직일 수 없는" 상태인지를 판정한다. (핵심은 1번 단독 통제 불가와 5번 이탈권이다.)

#기준테스트Aset
1단독 통제 불가운영자가 유저 없이 자산을 옮길 수 있나?불가(money-path 목적지가 고정돼 있다)
2재현 불가성서버가 통째로 뚫려도 자산이 안전한가?안전(유저 키를 보유하지 않는다)
3비저장·휘발성DB나 백업에 그대로 쓸 수 있는 키가 남나?남지 않는다
4인증 ≠ 권한인증만으로 자산에 접근할 수 있나?분리돼 있다
5이탈 가능성회사가 없어져도 유저가 자산을 통제할 수 있나?🔴 TODO(review, 2026-08-31). 이 판정은 문서 편집으로 끝나는 문제가 아니다. freeze 이탈권(v3-28)은 그대로이고 코어에 있다. 회차 풀은 무권한 executeEpoch와 claim-on-behalf를 유지한다. 즉시 풀은 이탈권을 잃었다. claimRedemptionFallback이 풀 reserve에서 정산했는데, reserve가 입금 시점에 reserve_wallet으로 라우팅되면서 그 함수가 제거됐다. 그래서 즉시 풀 홀더는 그 지갑이 자기 요청에 자금을 대 주는 것에 의존한다. 이는 컨트랙트의 보장이 아니라 운영상의 약속이다.
6복구 독립성운영자가 혼자서 복구 조각을 다 모을 수 있나?아니다(관리형 지갑 B안을 채택하면 재검증 필요)

2. 키 모델 (cold / hot)

역할권한제약
Cold: Admin SafeDEFAULT_ADMIN_ROLErole 관리, 거버넌스 propose/execute, lifecycle전부 타임락 · WIND_DOWN 직접 설정 차단 · Gnosis Safe 3-of-5(v3-37)
Cold: Pauser SafePAUSER_ROLEpause / 긴급 동결즉시 실행되지만 멈추는 것만 가능하고 자동 만료된다(v3-28) · 별도 Gnosis Safe 2-of-3(v3-37)
Hot(서비스 키)Aset Lambda(ORACLE_ROLE)updateNAV · approveRedemption · settleYield목적지 고정 → 단독 통제 불가
파트너fund_wallet(YIELD_DEPOSITOR_ROLE)depositYield · fundRedemption자금 유입만 가능(유출 불가)
Reserve(Aset 운영)reserve_wallet(RESERVE_FUNDER_ROLE)fundRedemption자금 유입만 가능. 모든 입금의 reserve 몫을 보유하고, v3-28의 온체인 이탈권을 대체한 SLA에 따라 즉시 풀의 부족분을 메운다(D4-a)

hot key가 상주해도 비수탁인 이유

보호의 원천은 키 비밀유지가 아니라 목적지 고정이다. 키를 탈취당해도 공격자는 정당한 보유자나 고정된 주소로만 보낼 수 있으므로 절도가 불가능하다(최악의 경우는 타이밍 방해나 DoS다). 그래서 단일 cold key 운영으로도 비수탁이 유지된다. 역할 상세는 09-rbac 참조.

3. Money-path 불변성 (v3-27)

전 구간 자금 지도와 정본 지갑 네이밍은 23-money-path에 있다.

유저 자금 경로(원금·수익)는 운영자가 setter로도 업그레이드로도 바꿀 수 없다. 우리 돈(수수료) 경로만 바꿀 수 있다.

항목변경 가능성근거(코드)
상환 지급 = request.investor🔒 불변요청 시 transferFrom으로 LP가 잠긴, 보유 검증된 주소다(요청 시 저장된 msg.sender). 이를 바꿀 setter가 없고 지급 함수에 목적지 파라미터가 없다. claim-on-behalf도 이 주소로 지급한다.
수익 청구 = msg.sender(분배는 보유량에서 파생)🔒 불변settleYield는 홀더 몫에 대해 수령자를 지정하지 않고 발생한 채무를 채울 뿐이다. claimYield는 호출자에게 지급하고, 금액은 그 사람의 발생 구간에서 유도된다
Reserve path거버넌스 변경 가능현재 소스의 외부 리저브 풀은 입금의 reserve_bps 몫을 reserve_wallet으로 보내고 나머지를 fund_wallet으로 보낸다. 풀 내부 리저브 잔고와 getter는 제거됐다. 리저브 비율은 보유 비율이 아니라 외부 지갑으로 보내는 비율이다. 외부 잔액이 풀의 지급 재원으로 자동 보충되지는 않는다. reserveWallet 변경에는 7일 타임락을 적용한다.
수수료 수령(treasury)🟡 변경 가능(7일 타임락)우리 돈이다. settleYield의 수수료 갈래는 unclaimedYield 상한과 고정 목적지(파라미터가 아니다)로 묶여 있어 원금이나 reserve에 닿을 수 없다
fund_wallet(파트너 잔여분 목적지)🟡 변경 가능(7일 타임락)파트너 지갑 변경(propose/execute)
오라클 주소 등 주변부🟡 변경 가능(타임락)운영상 필요하고, 오남용은 타임락으로 완화한다

원칙: 운영자가 설정할 수 있는 "투자자 지급 주소"라는 것이 애초에 존재하지 않는다. 유저 자금 목적지는 로직에 요청자 / 보유자로 하드코딩돼 있다.

4. 업그레이드 거버넌스: 코어는 불변, KYC만 업그레이드 가능 (v3-27 / v3-30)

  • 현재: PlatformPool은 **EIP-1167 Clones(최소 프록시)**라 업그레이드 메커니즘 자체가 없다. 코어 money-path는 사실상 불변이다.
  • 방향(v3-30에서 결정된 선택적 모듈화, 현재 구현됨): 코어 money-path는 불변으로 두고, 온체인에서 업그레이드 가능하게 만든 유일한 조각은 KYC 컨트랙트(PlatformKYCSoulbound)의 UUPS 프록시 + 타임락이다. 코드에 살아 있다(UUPSUpgradeable + _authorizeUpgrade 뒤의 proposeUpgrade/executeUpgrade). 나머지는 이미 오프체인에서 갱신 가능하다. 스테이블코인은 기존 add/removeStablecoin setter로, 오라클 소스·수수료·검증은 Lambda에서, updateNAV 한계는 불변 코어 안에서 처리한다.
  • 왜 KYC만인가: 완전 불변인 Clones에서는 온체인 변경이 곧 재배포와 모든 풀 마이그레이션을 뜻한다. KYC와 컴플라이언스는 온체인 조각 중 실제로 계속 변하는 유일한 부분이라, 그것만 업그레이드 가능하게 해서 컴플라이언스 변경에 풀 마이그레이션이 필요 없게 했다. 불변식: KYC 주변부는 적격성을 게이팅할 수 있을 뿐 자금을 옮기거나 지급 목적지를 바꿀 수 없다. 안전 한계는 코어에 남는다. KYC 업그레이드에는 타임락이 걸린다(이탈 창, 기준 5번).
  • ⚠️ 유의: KYC는 REVOKED(AML)일 때 상환을 막으므로, 업그레이드 가능한 KYC는 이탈권(5번)에 닿는다. 타임락과 "적격성만" 불변식으로 완화하되 법무·감사의 확인이 필요하다.
  • 트레이드오프: 코어 버그는 여전히 제자리에서 패치할 수 없어 새 풀을 배포하고 유저를 마이그레이션해야 한다. 불변성은 그 대가로 자금 방향 전환을 전 구간에서 막는다.
  • 멀티시그(v3-37): money-path가 업그레이드 불가라면 키가 하나여도 자금을 돌릴 수 없으므로 단일 cold key + 타임락만으로 비수탁이 성립한다(따라서 멀티시그가 필수는 아니고, 풀 생성 멀티시그 UI는 제거됐다). 그럼에도 2026-06-23 보안 리뷰 이후 방어를 겹치는 차원에서 Gnosis Safe를 채택한다. DEFAULT_ADMIN3-of-5 cold Safe, PAUSER별도 2-of-3 cold Safe다(긴급 동결을 빠르게 하려고 임계를 낮췄고, 동결은 멈추는 것만 가능하고 자동 만료되므로 안전하다). 임계값은 잠정이다. 설정 변경일 뿐 컨트랙트 변경은 없다(Admin과 Pauser는 이미 별개 role이고, 컨트랙트는 hasRole만 확인하므로 EOA인지 Safe인지 신경 쓰지 않는다).

즉시 실행되고 제한 없는 단독 업그레이드 권한이 있으면 "목적지를 공격자로 바꾸는 업그레이드"가 가능해지고, 그러면 사실상 수탁이다. 그래서 코어를 불변으로 만들었다.

5. 긴급 동결과 이탈권 (v3-28)

즉시 동결은 유지하되 무기한은 절대 아니다.

  • 이탈을 막는 전면 동결은 최대 72시간이고, 그 뒤에는 상환이 자동으로 허용된다
  • 그 외(입금 등) 동결 창은 7일이다
  • 연장과 해제는 거버넌스와 타임락으로만 가능하다(비대칭이다. 멈추기는 쉽고 가두기는 어렵다)

프리즈 시간 제한은 프리즈 해제 조건만 설명한다. 외부 지갑의 자금 투입이나 원금 지급을 보장하지 않는다.

6. 서비스 키의 제한된 조작

함수제약
approveRedemption원래 요청자(request.investor)에게 지급하는 게이트다. 목적지가 고정돼 있다
settleYield(net 갈래)보유량 비례로만 처리된다(수령자 파라미터가 없다)
settleYield(수수료 갈래)목적지가 불변인 treasuryWallet과 풀별 fundFeeWallet이다(파라미터가 아니고, treasury는 7일 타임락이다). 따라서 자금 방향을 바꿀 수 없고 reserveBalance나 원금에 닿을 수도 없다(별개 상태라 구조적으로 도달 불가). 불변이고 업그레이드 불가인 money-path가 이를 온체인에서 보장한다. 이들은 unclaimedYield에서 끌어가는데, 이는 총 수익 누적 계정을 공유하는 것이다(depositYield가 총액을 더하고, net 크레딧과 수수료 두 갈래가 그것을 깎는다). v3-102 이후 셋 모두 한 호출 안에서 그 상한에 대해 검사되므로, 호출 순서를 조작해 수수료 갈래가 수수료 몫을 넘길 수 없다. 분리가 Lambda의 관습이 아니라 구조가 됐다
updateNAVNAV ≤ 1.0(par) · 하락은 24시간 타임락 · ✅ reserveConsumed != 0이면 InvalidAmount로 revert한다(R8, 반영 완료). reserve는 오라클이 마침 0을 보내서가 아니라 구조적으로 NAV에서 도달 불가다. pendingReserveConsumed의 라이터는 그 가드 뒤에 하나뿐이라, applyPendingNav 안의 consumed > 0 차감은 죽은 코드다(제거 예정). ✅ 편차 상한(navDeviationCapBps), 서킷 브레이커(circuitBreakerTripped), staleness(navStalenessSeconds)가 코어에 구현돼 있다(v3-32). hot key가 호출하므로 그 한계가 불변 코어 안에 있다

7. 회수와 Wind-down

  • 파트너 부실이나 부재 시 청산 제안과 타임락을 거친다. 현재 executeWindDown()은 오라클 NAV를 유지한다. 외부 리저브 잔고로 가격을 다시 계산하지 않으며 실제 지급에는 별도 자금 투입과 상환 절차가 필요하다.
  • ⚠️ 온체인으로 회수 가능한 것은 reserve(약 10%)뿐이다. 나머지는 파트너 fund_wallet에 있어 온체인으로 회수할 수 없고 오프체인 법적 절차가 필요하다. (투자자 고지 사항)
  • 상세: 04-pool-models → 긴급 Wind-Down.

8. 규제 근거

  • 비수탁은 태국과 말레이시아에서 VASP·자금이체업 라이선스를 피하는 전제다(법무 확인 필요).
  • 다만 아주 짧게라도 Aset이 통제하는 hot wallet에 자금을 모으거나 float를 취하면 수탁으로 판단될 수 있다. (현재는 하지 않는다)
  • 자문 항목 상세: Legal Review Required A-1(VASP / 비수탁).

9. 미해결 항목

코어에 구현됨(2026-07-02에 apps/contract/src 대조로 확인):

  • updateNAV의 편차 상한(navDeviationCapBps), 서킷 브레이커(circuitBreakerTripped), staleness(navStalenessSeconds)가 불변 코어에 있다(v3-32)
  • ✅ 동결 시간 제한(FREEZE_EXIT_WINDOW = 72h, FREEZE_MAX_DURATION = 7d)(v3-28). 변경 없음
  • 🔴 무권한 타임락 상환 폴백(claimRedemptionFallback)(v3-31, 기준 5번). 그것이 끌어 쓰던 reserve와 함께 2026-08-31에 제거됐다. 회차 풀은 무권한 정산을 유지하고, 즉시 풀에는 그것이 없다. 5번 항목 참조.
  • PlatformKYCSoulbound는 UUPS 프록시이고 업그레이드에 타임락이 걸린다(proposeUpgrade/executeUpgrade)(v3-30). 코어 money-path 컨트랙트는 그대로다

여전히 미해결:

  • unclaimedYield의 범위. 2026-07-02 코드에서 확인: 수수료 전용 변수가 아니라 총 수익 누적 계정을 공유하는 것이다. 온체인 보장은 이렇다. 수수료 갈래는 reserveBalance나 원금(별개 상태)에 닿을 수 없고, 불변인 treasuryWallet / fundFeeWallet으로만 보낼 수 있어 유출이 불가능하다. ⚠️ 2026-07-31 갱신(v3-102): 수수료와 투자자 수익의 분리는 예전에 Lambda 호출 순서라는 불변식에 기대고 있었다(distributeYieldwithdrawFees보다 먼저). 이제 settleYield가 한 호출에서 net + fees ≤ unclaimedYield를 검사하므로 온체인 제약이 됐다. (이름을 unclaimedFees로 바꿀 수는 없다. depositYieldsettleYield 사이에 총액을 담고 있기 때문이다.)
  • ❓ v3-30의 유의점: 업그레이드 가능한 KYC(REVOKED 상환을 막는다)가 이탈권 5번을 훼손하지 않는지 법무·감사가 확인해야 한다
  • ✅ DEFAULT_ADMIN의 단독 role 부여. 받아들이기로 했다. 단일 cold key + 7일/30일 타임락이고 멀티시그는 없다(불변 money-path 때문에 절도가 불가능하고, 방해 행위는 v3-32가 경계를 잡는다)(v3-32)