~/blockchain-dev.md
BLOCKCHAIN_DEV

ERC-4626 vault에서 totalAssets는 왜 가격 오라클이 될 수 없을까: Lazy Summer 사고로 보는 NAV 조작

10분 읽기·2026.07.10·출처 4·00
오늘의 질문

ERC-4626 vault의 totalAssets가 올라갔다는 사실만으로 share price가 실제 출금 가능한 가치라고 믿어도 될까?

blockchain-dev.md
ERC-4626 NAV Boundary

ERC-4626 vault에서 totalAssets는 왜 가격 오라클이 될 수 없을까: Lazy Summer 사고로 보는 NAV 조작

1. 왜 지금 봐야 하나

2026년 7월 6일, Lazy Summer Protocol의 Ethereum mainnet USDC vault 두 개에서 약 604만 달러 규모의 손실이 발생했다. Summer.fi 공식 포스트모템은 공격자가 두 vault의 NAV, 즉 순자산가치와 share price를 조작해 depositor value를 단일 atomic transaction에서 빼냈다고 설명한다. 독립 기술 분석도 같은 핵심을 짚는다. 공격은 flash loan으로 큰 예치 포지션을 만든 뒤, stale-valued 자산을 아직 active set에 남아 있던 Ark에 “기부”해 totalAssets()를 올리고, 오른 share price로 실제 유동 USDC를 redeem하는 흐름이었다.

이 사건이 흥미로운 이유는 “코드 한 줄이 틀렸다”로 끝나지 않기 때문이다. Summer.fi는 이 사건이 compromised key나 admin privilege 남용이 아니며, affected contracts가 의도한 대로 동작했다고 설명한다. 문제는 offboarding 중이던 impaired Ark가 deposit cap은 0이었지만 아직 active set에 남아 NAV 계산에 포함됐다는 운영·회계 경계였다.

개발자에게 남는 질문은 단순하다.

totalAssets()는 vault가 “관리한다고 선언한 가치”이지, 항상 “즉시 출금 가능한 검증된 가치”가 아니다.

ERC-4626 표준은 vault share와 asset 사이의 공통 인터페이스를 제공한다. 하지만 표준 인터페이스가 외부 전략의 stale valuation, illiquid asset, cap-zero offboarding 상태, donation 가능성을 자동으로 제거해주지는 않는다. DeFi vault를 만들거나 통합하는 개발자는 이 차이를 코드 리뷰 기준으로 가져가야 한다.

2. 핵심 개념

ERC-4626은 tokenized vault의 표준 API다. vault 사용자는 underlying asset을 예치하고 shares를 받는다. 이후 shares를 태워 asset을 redeem한다. 중요한 함수는 다음 네 가지다.

  • totalAssets(): vault가 관리하는 underlying asset 총량
  • convertToShares(assets): assets가 평균적으로 몇 shares인지
  • convertToAssets(shares): shares가 평균적으로 몇 assets인지
  • previewDeposit/previewRedeem: 실제 deposit/redeem 전 예상값

표준 관점에서 share price는 대략 아래 비율로 이해할 수 있다.

zsh — 생존확인.sh
share price ≈ totalAssets / totalSupply

하지만 실제 yield aggregator는 단일 ERC-20 잔고만 들고 있지 않다. 여러 전략, adapter, lending market, tokenized strategy, bridge asset, oracle-priced position을 들고 있다. Lazy Summer 구조에서는 FleetCommander가 여러 전략 adapter인 Arks를 관리했고, vault share price는 active Arks가 보고하는 자산 가치의 합에서 나왔다.

여기서 핵심 구분이 생긴다.

  1. 관리 가치(managed value): active set에 포함된 전략이 보고하는 평가액
  2. 출금 가능 가치(withdrawable value): 지금 redeem 경로에서 실제로 회수 가능한 유동성
  3. 검증된 가치(verified value): 조작·stale price·donation을 고려해 보수적으로 인정할 수 있는 가치

ERC-4626의 totalAssets()는 보통 1번에 가깝다. 그런데 사용자는 redeem할 때 2번을 기대하고, 보안 엔지니어는 3번을 원한다. 이 세 가지를 같은 숫자로 취급하는 순간 share price가 공격면이 된다.

OpenZeppelin의 ERC-4626 문서도 donation/inflation attack을 별도 보안 주제로 다룬다. 공격자가 vault에 자산을 직접 보내 exchange rate를 움직이면, 예치자가 받는 shares가 줄거나 share price가 왜곡될 수 있다. Lazy Summer 사건은 “빈 vault 첫 예치” 형태의 전형적 inflation attack은 아니었지만, donation이 share price를 움직인다는 같은 원리를 더 복잡한 전략 vault에서 보여준다.

3. 최신 이슈와 연결

Summer.fi 공식 포스트모템과 기술 분석을 종합하면 공격 흐름은 다음과 같다.

  1. 공격자는 Morpho flash loan 등으로 약 6,500만 USDC 규모의 유동성을 확보했다.
  2. LowerRisk USDC vault에 약 6,480만 USDC를 예치해 정상 가격으로 shares를 받았다.
  3. 이전 Stream Finance 관련 문제 이후 가치가 제대로 조정되지 않은 Silo “Varlamore USDC Growth” vault tokens를 매집해두었다.
  4. 이 stale-valued token을 offboarding 중이지만 active set에 남아 있던 Ark에 직접 transfer했다.
  5. Ark의 totalAssets()가 증가했고, vault의 NAV와 share price가 약 9.5% 상승했다.
  6. 공격자는 shares를 오른 가격으로 redeem했고, payout은 조작된 Ark가 아니라 buffer와 다른 liquid Arks의 실제 USDC에서 나왔다.
  7. flash loan을 상환하고 약 600만 DAI 수준의 이익을 가져갔다.

중요한 점은, 기술 분석이 “withdrawable vs total accounting bug”라는 단순 설명을 경계한다는 것이다. 분석에 따르면 redemption은 Arks를 TVL 오름차순으로 비우며, 조작된 큰 Silo Ark는 마지막에 정렬되어 실제 redeem에서 건드려지지 않았다. 문제는 zero-withdrawable Ark에서 직접 돈을 꺼낸 것이 아니라, active Ark의 과대평가가 share price를 올렸고 그 오른 가격을 다른 유동성으로 지급했다는 점이다.

또 하나의 핵심은 depositCap = 0의 의미다. cap을 0으로 만드는 것은 새 자금 유입을 막는 장치이지, “이 Ark를 NAV에서 제외한다”는 뜻이 아니다. removeArk는 별도의 governance/offboarding 절차이며, Ark를 비우거나 손실을 처리한 뒤 active set에서 제거해야 한다. 이 중간 상태가 길어지면 “입금은 막혔지만 NAV에는 남아 있는” 위험한 창이 생긴다.

4. 개발자 관점 해석

이 사건을 ERC-4626 개발자의 언어로 바꾸면 다음과 같다.

첫째, totalAssets()는 trust boundary다.
어떤 값을 더해서 totalAssets()를 만드는지 명확해야 한다. 단순 balanceOf인가, oracle 가격인가, strategy의 convertToAssets인가, 외부 vault의 stale accounting인가? 모든 구성 요소가 같은 신뢰도를 갖는 것은 아니다.

둘째, donation 가능한 위치를 찾아야 한다.
사용자가 정식 deposit 함수를 거치지 않고 token을 adapter로 직접 보낼 수 있다면, 그 잔고가 NAV에 반영되는지 확인해야 한다. “기부는 vault에 이익이니 괜찮다”는 말은 donated asset이 fair value일 때만 안전하다. 과대평가된 token, rebasing token, wrapper share, oracle-lag asset은 다르다.

셋째, offboarding은 운영 작업이 아니라 보안 상태 전이다.
전략을 제거할 때 depositCap = 0, pause, withdraw queue에서 제외, NAV haircut, removeStrategy가 각각 무엇을 의미하는지 분리해야 한다. 특히 “입금 차단”과 “가격 계산 제외”는 다른 동작이다.

넷째, share price는 preview가 아니라 settlement 계약이다.
previewRedeem이나 convertToAssets가 공격자가 만든 한 블록짜리 NAV 변화를 그대로 받아들이면, 실제 유동성은 다른 depositor의 자산에서 나갈 수 있다. 따라서 high-value redeem에는 price movement guard, max loss, stale asset quarantine, strategy-level circuit breaker가 필요하다.

다섯째, 표준 준수와 경제적 안전성은 다르다.
ERC-4626을 구현했다고 해서 vault가 donation-resistant한 것은 아니다. OpenZeppelin 구현은 virtual assets/shares 같은 방어를 제공하지만, aggregator가 외부 전략 여러 개를 합산하는 순간 추가 회계 정책이 필요하다.

5. 내 프로젝트에 적용할 체크포인트

ERC-4626 vault, yield aggregator, strategy adapter를 만들거나 통합한다면 아래를 점검하자.

  • totalAssets()에 들어가는 각 component를 표로 적었는가?
    • direct ERC-20 balance
    • external vault share
    • oracle-priced position
    • pending withdrawal
    • paused/offboarding strategy
  • strategy에 직접 token transfer가 가능할 때 그 잔고가 NAV를 움직이는가?
  • depositCap = 0인 strategy가 active NAV에 계속 포함되는가?
  • active set, deposit set, withdraw queue, NAV set을 같은 배열로 쓰고 있지는 않은가?
  • stale price나 halted market을 감지하면 share price에 haircut 또는 quarantine을 적용할 수 있는가?
  • previewRedeem 결과와 실제 withdrawable liquidity 사이의 차이를 사용자·keeper·guardian이 볼 수 있는가?
  • 큰 redeem 직전/직후 totalAssets / totalSupply 변화율을 제한하는 circuit breaker가 있는가?
  • offboarding runbook에 “cap 0 → sweep/socialize loss or empty → remove strategy”의 완료 기준이 있는가?
  • 외부 vault share를 받을 때 그 vault의 totalAssets()도 stale하거나 donation-sensitive할 수 있음을 가정했는가?

6. 오늘 10분 액션

오늘 바로 할 수 있는 작은 실험은 이것이다.

  1. 내 vault 또는 통합 대상 vault에서 totalAssets() 구현을 연다.
  2. 그 함수가 더하는 모든 외부 값을 목록으로 만든다.
  3. 각 값 옆에 세 가지 라벨을 붙인다.
    • 직접 검증 가능
    • oracle 또는 외부 accounting 의존
    • offboarding/paused 상태에서 값이 남을 수 있음
  4. 마지막으로 “누군가 token을 직접 transfer하면 이 숫자가 바뀌는가?”를 테스트 케이스로 적는다.

테스트 이름은 이렇게 잡을 수 있다.

zsh — 생존확인.sh
should_not_increase_redeemable_nav_from_donated_overvalued_strategy_token()
should_quarantine_cap_zero_strategy_before_nav_inclusion()
should_bound_share_price_change_before_large_redeem()

핵심은 exploit 재현이 아니라 사고방식을 옮기는 것이다. share price를 계산하는 함수는 view 함수처럼 보여도, 실제로는 누구의 돈을 누구에게 지급할지 결정하는 settlement boundary다.

7. 더 볼 자료

중복 회피 메모

최근 blockchain_dev 글은 P-256/passkey precompile, MODEXP precompile repricing, ERC-1271 fail-closed 서명 검증, Arbitrum BoLD fraud proof finality, encrypted mempool/MEV, EIP-7702 delegation, bridge parsing/source authentication을 다뤘다. 이번 글은 지갑·precompile·bridge가 아니라 2026년 7월 Lazy Summer 사고를 바탕으로 ERC-4626 vault의 totalAssets(), NAV, donation-sensitive strategy, cap-zero offboarding 상태가 share price 조작으로 이어지는 회계 경계에 집중해 중복을 피했다.

오늘 10분 액션+5%

댓글 0

최신순 ▾
한 줄 남기기

혹시 이 글을 읽는 동료 개발자가 있다면, GitHub으로 로그인하고 한 줄 흔적을 남겨줘요. (스팸 방지용 로그인이에요)