지난 한 주(2026/04/20 - 2026/04/26) 동안 BlockSec은 8건의 공격 사건을 탐지 및 분석했으며, 총 추정 피해액은 약 $7.04M입니다. 아래 표는 해당 사건들을 요약하며, 각 사례에 대한 상세 분석은 이후 소절에서 제공됩니다.
| 날짜 | 사건 | 유형 | 추정 피해액 |
|---|---|---|---|
| 2026/04/19* | Custom Rebalancer Contract | 임의 호출(Arbitrary Call) | ~$64K |
| 2026/04/20 | REVLoans (Juicebox) | 부적절한 검증(Improper Validation) | ~$50.7K |
| 2026/04/22 | Volo Vault / Navi | 키 탈취(Key Compromise) | ~$3.5M |
| 2026/04/22 | Kipseli Router | 부적절한 검증(Improper Validation) | ~$72.35K |
| 2026/04/23 | GiddyDefi | 불완전한 서명 검증(Incomplete Signature Validation) | ~$1.3M |
| 2026/04/25 | Purrlend | 키 탈취(Key Compromise) | ~$1.5M |
| 2026/04/26 | SingularityFinance | 오라클 잘못된 설정(Oracle Misconfiguration) | ~$413K |
| 2026/04/26 | Scallop | 회계 결함(Accounting Flaw) | ~$142.7K |
*Custom Rebalancer Contract 사건은 지난 주 보고서에 포함되지 않았으며, 완전성을 위해 여기에 포함됩니다.
주간 하이라이트: GiddyDefi
공격자는 서명을 해독하지도, 플래시 론을 사용하지도, 가격을 조작하지도 않았습니다. 그들은 서명되지 않은 필드를 자신의 컨트랙트로 교체하여 합법적인 서명을 재사용했습니다. "당신 자신의 서명을 당신에게 불리하게 사용"하는 것은 부분적인 EIP-712 커버리지가 유효한 서명을 범용 허가증으로 만드는 방식을 가장 명확하게 보여주는 사례입니다.
2026년 4월 23일, 이더리움의 GiddyVaultV3가 약 $1.3M의 피해를 입었습니다. 서명 체계가 SwapInfo.data만 커버했으며, aggregator, fromToken, toToken, amount는 EIP-712 해시에서 제외되었습니다. 따라서 유효한 서명을 해당 필드가 변조된 상태로 재사용할 수 있었습니다. 공격자는 aggregator를 악성 컨트랙트로, fromToken을 전략의 LP 토큰으로 지정하여 약 $1.3M을 탈취했습니다.
배경
GiddyVaultV3 (0x5f0a...4318)는 사용자가 deposit()과 withdraw()를 통해 자금을 입출금하는 수익 농사(yield farming) 볼트 컨트랙트입니다. 모든 작업에는 백엔드가 서명한 VaultAuth 인가 구조체가 포함되어야 하며, 여기에는 EIP-712 서명과 토큰 스왑 경로를 설명하는 SwapInfo[ ] 배열이 포함됩니다. 스왑 실행 시 컨트랙트는 GiddyLibraryV3.executeSwap()을 호출하며, 이는 swap.fromToken에 대해 swap.aggregator에 허용량을 부여하는 forceApprove를 수행한 후 aggregator.call(swap.data)를 통해 스왑을 실행합니다. 이후 전략 컨트랙트가 설정된 전략에 따라 자금을 관리합니다.
EIP-712는 구조화된 오프체인 데이터에 서명하기 위한 표준입니다. 서명을 사용하는 프로토콜은 온체인에서 동일한 구조체를 재구성하고, 합의된 도메인 구분자 하에서 해시를 생성한 후 서명자의 주소를 복구합니다. 따라서 EIP-712 흐름의 보안은 실행에 영향을 미치는 모든 필드를 온체인 해시가 포함하느냐에 달려 있습니다. Giddy의 설계에서 백엔드는 사용자의 의도와 필요한 스왑의 라우팅 지침을 모두 포함하는 VaultAuth에 서명하며, _validateAuthorization()은 전략이 자금을 이동할 수 있도록 허용하기 전에 서명을 검증하기 위해 해당 구조체를 재구성합니다.
취약점 분석
취약점은 GiddyVaultV3의 _validateAuthorization() 함수에 있습니다. 서명된 페이로드를 구성할 때 각 SwapInfo의 data 필드만 해시되며, aggregator, fromToken, toToken, amount는 모두 서명에서 제외됩니다. 이는 유효한 서명을 보유한 누구든 서명 검증을 통과하면서 SwapInfo의 나머지 필드를 자유롭게 교체할 수 있음을 의미합니다.
제외된 각 필드는 서명이 자유롭게 남겨두는 별개의 레버입니다. aggregator는 forceApprove와 aggregator.call(swap.data)를 통해 지출자이자 호출 대상이 됩니다. fromToken은 어떤 전략 자산이 승인될지 선택하고, amount는 허용량 상한을 설정하며, toToken은 returnAmount > 0 검사에만 사용됩니다. 서명된 data는 이들 중 어느 것도 제약하지 않는데, 이들 대상 중 어느 것도 그 안에서 참조되지 않기 때문입니다.

공격 분석
다음 분석은 트랜잭션 0x5edb66...5482e5를 기반으로 합니다.
-
1단계: 공격자는 온체인에서 백엔드가 합법적으로 인가한
VaultAuth서명을 획득했으며,data필드는 그대로 유지했습니다. 모든 이전deposit()또는withdraw()호출이 전체VaultAuth페이로드를 온체인에 브로드캐스트하기 때문에, 과거 트랜잭션은 재사용 가능한 서명의 무료 소스였습니다. 공격자는 의도한 스왑 호출에 적합한data필드를 가진 서명 하나만 필요했습니다. -
2단계: 획득한 서명을 사용하여 공격자는 원래의
signature,nonce,data를 변경하지 않고 나머지 필드를 변조했습니다.fromToken은 전략 컨트랙트가 보유한 LP 토큰(실제 자산)으로 설정되어forceApprove가 프로토콜이 실제로 보유한 토큰에 대한 허용량을 부여하도록 했습니다.aggregator는 공격자의 악성 컨트랙트로 교체되어 승인과 이후aggregator.call()이 공격자 소유 코드로 향하도록 했습니다. 이 필드들이 서명 검증 범위 밖에 있었기 때문에_validateAuthorization()은 변조된 구조체를 그대로 수락했습니다. 최종require(returnAmount > 0, "SWAP_NO_TOKENS_RECEIVED")검사를 우회하기 위해 악성 aggregator는 프로토콜에 가짜 토큰을 다시 민팅하는 함수를 구현하여 실제 스왑 없이 검사를 통과시켰습니다.


- 3단계: 악성 aggregator가 2단계에서 승인을 받았으므로, 공격자는
transferFrom을 호출하여 볼트의 LP 토큰을 악성 aggregator로 직접 이동시켜 탈취를 완료했습니다. 이 단계는 프로토콜의 보호된 실행 경로 밖에 있었습니다.executeSwap()이 반환될 때쯤에는 허용량이 이미 기록되고 호출 후 잔액 검사도 이미 통과된 상태였으므로, 프로토콜에는 더 이상 개입할 기회가 없었습니다.

결론
이 공격의 근본 원인은 불완전한 EIP-712 서명 커버리지였습니다. 자금 흐름을 직접 제어하는 SwapInfo의 핵심 필드들이 보호되지 않아, 공격자가 유효한 서명을 제시하면서 스왑 경로와 aggregator 주소를 대체할 수 있었습니다. 외부 aggregator를 통합하는 개발자는 다음 사항을 준수해야 합니다:
-
EIP-712 서명이
aggregator,fromToken,toToken,amount를 포함하여 실행 결과에 영향을 미치는 모든 필드를 커버하도록 합니다. -
감사되지 않은 외부 컨트랙트 호출을 방지하기 위해 aggregator 화이트리스트를 적용합니다.
-
가짜 토큰이 잔액 검사를 우회하는 것을 방지하기 위해
toToken을 예상된 기본 토큰으로 제한합니다.
더 넓게 보면, 승인 후 호출(approval-then-call) 아키텍처에서 EIP-712는 사용자에게 보이는 의도뿐만 아니라 결과적인 온체인 상태에 영향을 미치는 모든 필드를 해시해야 합니다. 백엔드 서명이 사용자 제공 매개변수와 권한 있는 컨트랙트 액션 사이의 유일한 게이트키퍼인 경우, 해당 액션으로 흘러가는 모든 매개변수(호출 대상, 자산, 금액, 수신자)는 서명된 구조체 안에 있어야 합니다. data를 호출의 식별자로 취급하는 것은 범주 오류입니다. 호출의 식별자는 모든 매개변수의 튜플이며, 서명 밖에 남겨진 매개변수는 정의상 트랜잭션을 제출하는 사람이 제어하게 됩니다.
Web3 최고의 보안 감사 기관
출시 전 설계, 코드, 비즈니스 로직을 검증하세요
이번 주 추가 사건들
Custom Rebalancer Contract
2026년 4월 19일, 아발란체(Avalanche)의 sAVAX 리밸런서 컨트랙트가 사용자의 Aave V3 신용 위임에서 약 $64K(~7,000 WAVAX)를 탈취하는 방식으로 악용되었습니다. 공개 함수가 사용자의 위임을 보유한 상태에서 임의의 target.call(data)를 실행했기 때문에, 공격자는 onBehalfOf를 피해자로 설정하여 Aave의 borrow()를 호출할 수 있었습니다. 화이트햇 봇이 공격을 선점하여 출금 전에 자금을 회수했습니다.
배경
리밸런서 컨트랙트 (0x7a7b...a8c9)는 Aave에서 사용자의 레버리지 포지션을 리밸런싱하도록 설계된 b2a13230() 함수를 노출합니다. 이 함수는 Aave V3 신용 위임을 통해 사용자를 대신하여 작동합니다. 사용자는 리밸런서에게 자신을 대신하여 차입할 권한을 부여하며, 리밸런서는 그 차입금과 사용자 제공 자금을 결합하여 포지션을 조정합니다(예: 차입 + 공급 워크플로우).
취약점 분석
근본 원인은 b2a13230()에 target.call(data) 단계가 포함되어 있으며, 대상과 콜데이터가 모두 호출자가 제어한다는 점입니다. 이 호출은 컨트랙트가 사용자의 Aave V3 신용 위임 하에서 여전히 운영되는 동안 실행되므로, 해당 단계에서 호출된 모든 로직은 사용자의 차입 능력을 상속합니다. 허용된 대상의 허용 목록도 없고 콜데이터에 대한 형태 제약도 없기 때문에, onBehalfOf를 사용자로 설정한 Aave의 borrow()를 포함하여 어떤 컨트랙트 메서드도 호출할 수 있습니다.

공격 분석
다음 분석은 트랜잭션 0xaaa1b2...35001b를 기반으로 합니다.
-
1단계: 공격자는 일정량의
sAVAX와USDC를 대상으로 플래시론을 실행했습니다. 이후 리밸런서 컨트랙트를 통해 빌린USDC를 Aave V3에 공급하여 차입을 위한 충분한 담보를 확보했습니다. 한편, 빌린sAVAX는 차입 후 공급 단계를 준비하기 위해 리밸런서 컨트랙트로 직접 전송되었습니다. -
2단계: 공격자는
b2a13230()함수를 호출했습니다. 이 함수는 먼저 정상적인 차입 작업을 수행한 후 임의 호출 섹션에 도달했습니다. 이 시점에서 공격자는onBehalfOf를 피해자 주소로 설정하여 Aave V3의borrow()함수를 직접 호출하도록 호출을 조작했습니다. 피해자가 리밸런서 컨트랙트에 신용 위임을 부여했기 때문에 차입이 성공했습니다. 차입된WAVAX가 리밸런서 컨트랙트로 전송되었습니다.

- 3단계: 공격자는
b2a13230()함수를 다시 호출했으며, 이번에는 리밸런서를 사용하여 자신을 대신해WAVAX를 차입했습니다. 컨트랙트는 이전에 차입한WAVAX(피해자의 포지션에서 유래한)를 공격자의 포지션에 공급하고 상환하는 데 사용하여, 공격자가 이익을 추출할 수 있게 했습니다.
결론
결함은 위임된 신용을 보유한 권한 있는 컨텍스트 내의 임의 외부 호출의 조합입니다. 각 레이어만 단독으로는 안전할 것입니다. 제약된 외부 호출은 위임을 악용할 수 없고, 위임 없는 임의 호출은 사용자의 자금을 이동시킬 수 없습니다. 신용 위임을 보유한 컨트랙트는 임의 외부 호출을 절대 노출해서는 안 됩니다. 그러한 호출이 필요한 경우, 대상은 허용 목록에 고정되고 콜데이터 형태가 검사되어야 합니다.
REVLoans (Juicebox)
2026년 4월 20일, Juicebox의 차입 확장 기능인 REVLoans가 이더리움에서 약 $50.7K의 피해를 입었습니다. borrowFrom()은 해당 쌍이 프로토콜에 등록되어 있는지 확인하지 않고 호출자가 제공한 회계 소스를 수락했습니다. 위조된 36자리 소수점 컨텍스트가 동일 통화 단축 경로를 트리거하여 잔액을 1e18배 잘못 재조정했습니다. 두 번의 트랜잭션 — 하나는 부풀려진 회계 항목을 심기 위한 것, 다른 하나는 부풀려진 주가로 합법적인 풀에서 차입하기 위한 것 — 으로 21.77 ETH를 탈취했습니다.
배경
Juicebox는 이더리움의 하이브리드 크라우드펀딩 및 대출 프로토콜입니다. 각 프로젝트는 자체 ERC20 주식 토큰(여기서는 REV로 지칭)과 하나 이상의 터미널에 분산된 재무를 보유하며, 터미널은 프로젝트 자산의 일부를 물리적으로 보관하고 사용자 대면 입출금 지점 역할을 하는 컨트랙트입니다. 하나의 프로젝트는 JBDirectory에 등록된 여러 터미널을 가질 수 있으며, 모든 (터미널, 프로젝트, 토큰) 트리플은 해당 터미널 내 해당 토큰의 장부 기록에 사용되는 (소수점, 통화)를 선언하는 JBAccountingContext를 가집니다. 따라서 REV는 단일 터미널에 대한 청구권이 아니라 프로젝트의 모든 터미널에 걸친 잉여금의 합에 대한 청구권입니다.
사용자는 새로 민팅된 REV를 받기 위해 터미널에 자산을 입금하거나, 잉여금의 비례적 몫(설정 가능한 현금 출금 세금으로 일부 가치를 남은 보유자에게 남겨둠)을 위해 터미널에서 REV를 환매할 수 있습니다. 그 위에 레이어된 별도 컨트랙트인 REVLoans (0x2db6...1846)는 차입 기능을 추가합니다. 사용자는 REV를 담보로 소각하고 프로젝트의 터미널 중 하나에 대한 대출을 받으며, 나중에 담보를 재민팅하는 대가로 대출을 상환할 수 있습니다. 차입 금액은 환매와 동일한 수학으로 가격이 책정되므로, 차입은 경제적으로 동일한 담보를 현금화하는 것과 동등합니다.
REV의 주가는 (총잉여금 + 총차입금) / (REV.총공급량 + 총담보)입니다. 분자에 총차입금을 포함시키면 차입/상환이 가격 중립적으로 유지됩니다. 이는 또한 부풀려진 총차입금이 주가를 직접 올려 소량의 담보가 불균형적으로 현금화될 수 있음을 의미합니다.
취약점 분석
근본 원인은 source 매개변수에 대한 검증되지 않은 입력입니다. borrowFrom()은 이 쌍이 주어진 revnetId에 등록되어 있는지 확인하지 않고 호출자가 제공한 REVLoanSource source(.terminal과 .token 필드를 가진 구조체)를 수락합니다. 두 필드 모두 현금 출금 수학으로 직접 흘러가므로, source.terminal이 반환하는 회계 컨텍스트는 완전히 호출자가 제어합니다. 해당 컨텍스트의 currency 필드가 목적지 터미널의 것과 일치하면, 프로토콜은 동일 통화 단축 경로를 취하고 가격 오라클을 건너뛰며 제공된 소수점과 잔액 수치를 권위 있는 것으로 취급합니다.

검증되지 않은 source는 _addTo()에 의해 _loanSourcesOf[revnetId]와 totalBorrowedFrom[revnetId][source.terminal][source.token]에 기록되며, 이 역시 등록 확인을 수행하지 않습니다.

(source, revnetId)가 장부에 기록되면, _borrowableAmountFrom()은 차입 요청을 지급 가능한 금액으로 변환하는 함수입니다. 이 함수는 _totalBorrowedFrom()에서 잉여금 = 총잉여금 + 총차입금을 구성한 후, 해당 잉여금을 호출자의 담보 수량 및 주식 공급량과 함께 JBCashOuts.cashOutFrom()으로 전달합니다.

소수점 버그는 한 단계 더 깊은 _totalBorrowedFrom()에 있습니다. 이 함수는 _loanSourcesOf를 반복하며 각 항목을 mulDiv(tokensLoaned, 10**decimals, pricePerUnit)를 통해 접습니다. 동일 통화 경로에서 pricePerUnit = 10**decimals(대상의 18자리 정밀도)이므로, 공식은 tokensLoaned로 변환 없이 축소되며, 36자리 회계로 저장된 잔액이 18자리 ETH 합계에 1e18배 크게 들어갑니다.

증폭은 cashOutFrom()에서 발생합니다. base = mulDiv(잉여금, cashOutCount, 총공급량): 잉여금이 부풀려진 총차입금에 의해 지배되면, 아주 작은 cashOutCount(담보)도 불균형적으로 큰 지급액으로 매핑됩니다.

공격 분석
공격은 두 개의 트랜잭션을 사용합니다. 첫 번째는 REVLoans의 장부를 오염시킵니다: 0xc46cb7...dead1f. 두 번째는 합법적인 터미널에 대해 풀을 탈취합니다: 0x9adbd6...a8f938.
- 1단계: 공격자는 대출 소스의
terminal과token모두 가짜 컨트랙트를 가리키도록 하여borrowFrom()을 호출하고, 소량의REV를 담보로 제공했습니다. REVLoans는 제공된 터미널이 revnet에 등록되어 있는지, 토큰이 터미널에서 인식되는지 확인하지 않았습니다.

- 2단계: REVLoans는 가짜 터미널에 회계 컨텍스트를 조회했으며, 위조된
(decimals=36, currency=ETH-code(61166))가 반환되었습니다. 소스와 대상 통화가 일치했기 때문에 REVLoans는 동일 통화 단축 경로를 취하고 가격 오라클을 건너뛴 후, 합법적인 터미널의 실제ETH잉여금을 공격자의 36자리 대상 단위로 재표현하여 현금 출금 수학을 실행했으며, 수치를 1e18배 부풀렸습니다.

- 3단계: REVLoans는
(가짜 터미널, 가짜 토큰)을_loanSourcesOf에 등록하고 부풀려진 수치를totalBorrowedFrom에 기록했습니다. 가짜 터미널은 단순히 수령을 확인하며 "지급"했습니다. 실제ETH는 이동하지 않았습니다. 첫 번째 트랜잭션은총차입금이 위로 조작되고 소량의REV담보만 소각된 상태로 종료되었습니다.

- 4단계: 공격자는 이번에는 합법적인
ETH터미널을 대출 소스로 전달하고 소량의REV담보를 제공하며borrowFrom()을 다시 호출했습니다. 현금 출금 수학은 실제 18자리ETH단위로 실행되었습니다.

- 5단계:
총차입금을 계산하는 동안 REVLoans는_loanSourcesOf를 반복하다가 3단계의 항목에 도달했습니다. 해당 항목의currency가 여전히ETH와 일치했기 때문에 동일 통화 단축 경로가 다시 발동되어 36자리 저장 잔액이 18자리ETH합계에 1e18배 크게 접혔습니다.총차입금은 이제 가짜 부채로 지배되었고 주가 분자가 대규모로 부풀려졌습니다.

- 6단계: 현금 출금 수학은 공격자가 합법적인 터미널의 실제 잉여금 바로 아래에 맞도록 사전 조정한 부풀려진 분자에 맞게 크기가 조정된 차입 금액을 반환했습니다. 합법적인 터미널이 이를 지급하여 거의 전체 풀을 공격자에게 탈취당했습니다.

결론
근본 원인은 두 가지 복합적인 취약점입니다. (터미널, 토큰) 쌍이 revnet 등록 확인 없이 수락되며, 동일 통화 단축 경로가 소스 잔액을 소수점 차이 없이 대상 합계로 접습니다. 각 취약점 단독으로는 덜 위험합니다. 그러나 함께 결합되면 호출자가 임의의 totalBorrowedFrom 항목을 주입하고 액면가로 현금화할 수 있게 됩니다. 완화 방법: (터미널, 토큰)을 revnet에 등록된 터미널에 대해 검증하고, 접기 전에 소스의 저장된 소수점 스케일로 잔액을 정규화합니다.
Volo Vault
2026년 4월 22일, Sui의 수익 볼트인 Volo는 사용자 예치금을 Navi 대출 프로토콜로 라우팅하여 대출 수익을 얻는 방식으로 운영되다가, 운영자 개인키가 유출된 후 약 $3.5M의 피해를 입었습니다. 볼트 컨트랙트에는 코드 수준의 버그가 없었습니다. 공격자는 단순히 탈취한 자격 증명으로 합법적인 운영자 경로를 실행하여 Volo의 Navi 예치금을 탈취했습니다.
배경
Volo는 사용자 대면 볼트(0xcd86...27fefa)이며, Navi는 기반 대출 프로토콜입니다. 볼트는 Navi AccountCap(Volo의 Navi 계정에서 출금을 승인하는 Sui 능력 객체)을 보유하고 전략 이동을 운영자 역할에 위임합니다. Navi에서 입출금하기 위해 운영자는 start_op_with_bag_v2()를 호출하여 볼트에서 임시 백으로 AccountCap을 꺼낸 후, deposit_with_account_cap() / withdraw_with_account_cap_v2()가 해당 캡을 사용하여 자금을 이동합니다.
취약점 분석
근본 원인은 컨트랙트 수준의 취약점이 아닌 운영/키 보관 실패입니다. Volo 전략 경로는 운영자 개인키를 보유한 사람에게 출금 권한을 위임합니다. start_op_with_bag_v2()는 두 가지 확인만 수행합니다(assert_operator_not_freezed(operation, cap)와 assert_single_vault_operator_paired(operation, vault.vault_id(), cap)). 두 확인 모두 제공된 능력이 등록된 운영자인지만 검증합니다. 이후 withdraw_with_account_cap_v2()는 꺼낸 AccountCap을 제시할 수 있는 모든 호출자를 수락합니다. 따라서 운영자 개인키를 소유한 사람은 합법적인 작업이 사용하는 것과 동일한 경로를 구별 없이 실행할 수 있습니다.

공격 분석
다음 분석은 트랜잭션 AQw9wM...3RUS를 기반으로 합니다.
- 1단계: 공격자는 유출된 운영자 키로
@volosui/volo-vault::operation의start_op_with_bag_v2를 호출하여 NaviAccountCap을 임시 백으로 꺼냈습니다.

-
2단계: 공격자는
bag::remove를 사용하여 임시 백에서AccountCap을 추출했습니다. -
3단계: 공격자는 추출된
AccountCap으로@navi-protocol/lending::incentive_v3의withdraw_with_account_cap_v2를 호출하여 Navi에서 Volo의 예치금을 인출했습니다.

- 4단계: 공격자는
bag::add를 사용하여AccountCap을 다시 넣고, 작업을 종료한 후 자금을 이체했습니다.
결론
결함은 구조적입니다. 단일 운영자 키, 완전한 출금 권한, 추가 확인 없음. 세 가지 변경으로 키 탈취로 인한 피해를 줄일 수 있습니다. 운영자 역할을 멀티시그 또는 임계값 체계로 분할하면 유출된 키만으로는 출금을 승인할 수 없습니다. 나가는 출금에 타임락을 추가하면 비정상적인 호출이 결제 전에 이의 제기 가능한 창을 갖게 됩니다. 운영자의 권한을 예치금 및 리밸런싱만으로 제한하고 사용자 대면 출금을 별도 경로로 라우팅하면, 운영자 역할이 사용자 자금에 접근하는 것을 완전히 방지할 수 있습니다.
Kipseli Router
2026년 4월 22일, Base의 Kipseli Router가 약 $72.35K의 피해를 입었습니다. 라우터는 외부 USDC 전용 견적 기관이 반환한 견적을 출력 토큰 전송 금액으로 그대로 사용하면서, 출력 토큰이 견적 토큰과 같은지 확인하지 않았습니다. 공격자는 견적 기관이 실제로 지원하지 않는 경로에서 0.04 WETH를 cbBTC로 스왑하여, 견적 기관의 USDC 스케일 반환값(92,610,395)을 원시 cbBTC 단위(≈0.926 cbBTC)로 받았습니다.
배경
Kipseli Router (0x579f...9a07)는 외부 견적 시스템을 기반으로 하는 스왑 실행 컨트랙트입니다. 컨트랙트는 오픈소스가 아니며, 아래 분석은 역컴파일된 바이트코드를 기반으로 합니다. 이 때문에 함수 이름이 4바이트 선택자(0xcce096f3(), 0x592(), 0xd88())로 나타납니다. 온체인 AMM 풀에서 직접 스왑 가격을 계산하는 대신, 출력 금액(amountOut)을 견적 기관에 조회한 후 해당 값을 기반으로 토큰 전송을 실행합니다. 정상 운영 시 사용자는 프로토콜 지갑에 tokenIn을 보내고, 라우터는 동일한 지갑에서 tokenOut을 꺼내 수신자에게 전달합니다. 프로토콜은 단일 QUOTE_TOKEN으로 구성되며, 견적 로직은 6자리 회계를 사용하는 USDC로 표시됩니다. 시스템은 USDC 표시 견적만 지원하도록 설계되었습니다.
취약점 분석
결함은 두 레이어에 걸쳐 있으며 서로 복합적으로 작용합니다. 라우터 측에서는 함수 0xcce096f3()이 견적 함수 0x592()를 통해 견적 v0를 검색하고 이를 변경 없이 0xd88()에 tokenOut.transferFrom(_wallet, receiver, v0)으로 전달합니다. 라우터는 tokenOut이 프로토콜의 QUOTE_TOKEN과 같은지 확인하지 않으므로, USDC 스케일 값(6자리 정밀도)이 cbBTC 수량(8자리 정밀도)인 것처럼 전송됩니다. 견적 기관 측에서는 기반 PropAMM AMM이 토큰 대 USDC 쌍 전용으로 설계되었지만, 지원되지 않는 라우팅 경로(WETH → cbBTC)를 되돌리지 않고 수락하며, tokenIn을 무시하고 스왑이 유효한 것처럼 USDC 스케일 값을 반환합니다.

공격 분석
다음 분석은 트랜잭션 0x96edee...3db3bb를 기반으로 합니다.
- 1단계: 공격자는
tokenIn=WETH,tokenOut=cbBTC로 라우터를 호출했습니다. 기반 AMM은 이 경로를 지원하지 않았지만 되돌리지 않았으며, 견적 기관0x592()는 92,610,395(≈92.61USDC)의USDC스케일 값을 반환했습니다.

- 2단계: 라우터는 해당 값을
cbBTC전송 금액으로 직접 사용했습니다.transferFrom을 통해 0.04WETH(≈$95)가 유입되었고, 프로토콜 지갑에서 공격자에게 92,610,395 원시cbBTC단위(≈0.926cbBTC, ≈$72.35K)가 유출되었습니다.

결론
두 가지 가정이 견적 기관 호출의 양쪽에서 확인되지 않았기 때문에 익스플로잇이 발생했습니다. 견적 기관은 자신의 출력이 자체 USDC 6자리 프레임에서 소비된다고 가정합니다. 라우터는 견적 기관이 반환하는 것이 요청된 tokenOut으로 표시된다고 가정합니다. 다음 수정 중 하나만으로도 버그를 제거할 수 있습니다:
-
라우터에서:
tokenOut == QUOTE_TOKEN을 assert하거나, 전송 전에 오라클을 통해USDC스케일 견적을tokenOut단위로 변환합니다. -
견적 기관에서: 지원되는 쌍 집합에 등록되지 않은 토큰의 라우팅 경로에서
USDC스케일 폴백을 조용히 반환하는 대신 되돌립니다.
Purrlend
2026년 4월 25일, HyperLiquid와 MegaETH의 대출 프로토콜인 Purrlend가 개인키 탈취로 인해 약 $1.5M의 피해를 입었습니다. 공격자는 브리지 역할을 탈취하여 담보 없는 pToken(Purrlend의 Aave와 유사한 영수증 토큰)을 민팅한 후, 그 pToken을 담보로 사용하여 풀에서 실제 자산을 차입했습니다.
배경
Purrlend (0x81d5...a702)는 Aave와 유사한 회계 모델을 가진 대출 프로토콜입니다. 사용자가 프로토콜에 자산을 공급하면 Aave의 aToken과 유사한 pToken을 받으며, 이는 그들의 공급 포지션을 나타내고 다른 자산 차입의 담보로 사용될 수 있습니다.
프로토콜에는 pool admin, risk admin, bridge를 포함한 권한 있는 역할도 포함됩니다. 브리지 역할은 크로스체인 회계를 위한 것입니다. 상대방 체인에서 발생한 예치금을 반영하기 위해 pToken을 민팅할 수 있습니다. 다른 관리자 역할은 위험 매개변수를 수정하고 차입 가능한 자산을 구성합니다.
취약점 분석
즉각적인 트리거는 권한 있는 키 탈취였습니다. 공격자는 Purrlend의 관리자 및 브리지 역할을 제어하는 키를 획득했습니다. 컨트랙트 수준의 설계 결함이 유출을 증폭시켰습니다. bridge 역할의 pToken 민팅 경로는 크로스체인 에스크로의 검증 가능한 증명에 고정되어 있지 않습니다. 이 함수는 브리지 역할을 가진 호출자가 소스 체인에서 해당 예치금이 발생했는지 확인하지 않고 어떤 주소에든 어떤 양의 pToken을 발행할 수 있도록 합니다. 프로토콜의 다른 곳에서 pToken은 유효한 담보로 취급되며, 차입 경로는 차입 시 담보를 재확인하지 않습니다. 따라서 승인되지 않은 브리지 역할 민팅은 민팅과 자산 출금 사이에 추가 게이트 없이 직접 차입 능력으로 변환됩니다.
공격 분석
다음 분석은 MegaETH의 트랜잭션 0xb96cff...dbbf24를 기반으로 합니다.
- 1단계: 공격자는 탈취된 권한 있는 키를 보유하고,
GnosisSafeProxy를 통한MultiSendCallOnly배치를 사용하여ACLManager를 통해 자신을pool admin,risk admin,bridge,emergency admin으로 설정한 후WETH를 차입 가능 자산으로 활성화하고BorrowCap을 200으로 설정했습니다.

-
2단계: 브리지 역할로 공격자는 자신의 주소에 대량의
pToken을 민팅했습니다. 브리지 민팅 경로는 크로스체인 에스크로 검증을 수행하지 않았으므로, 새로운pToken에는 이를 뒷받침하는 기반 자산이 없었습니다. -
3단계: 공격자는 담보 없는
pToken을 담보로 사용했습니다. 차입 경로는 담보를 재확인하지 않고pToken잔액을 유효한 공급 포지션으로 취급하기 때문에, 담보 확인이 통과되고WETH가 풀에서 차입되었습니다.
결론
이는 컨트랙트 수준의 설계 결함으로 증폭된 개인키 탈취였습니다. 유출된 키는 공격자에게 브리지 역할의 의도된 권한만 부여했지만, 그 권한에는 제약 없는 pToken 민팅이 포함되었으며 이는 차입 가능한 담보로 직접 변환됩니다. 각 레이어는 독립적으로 강화될 수 있습니다. 운영 레이어에서는 브리지 역할을 멀티시그 또는 임계값 체계로 분할하여 단일 키 유출로는 이를 실행할 수 없도록 합니다. 컨트랙트 레이어에서는 브리지 민팅이 검증 가능한 에스크로 증명(예: 신뢰할 수 있는 크로스체인 검증자의 메시지 커밋먼트)을 포함하도록 요구하고 증명이 없을 때 되돌립니다. 민팅 시점에 증명을 검증하는 것이 더 내구성 있는 수정인데, 키 보관에 대한 의존성을 완전히 제거하기 때문입니다.
SingularityFinance
2026년 4월 26일, Base의 SingularityFinance dynBaseUSDCv3 볼트가 약 $413K의 피해를 입었습니다. 볼트는 유효하지 않은 Uniswap V3 수수료 등급(V3에 존재하지 않는 42)으로 구성되었기 때문에, USDC가 아닌 모든 자산의 가격 오라클이 존재하지 않는 풀로 해석되었습니다. 가격 책정 함수는 되돌리는 대신 조용히 0을 반환했고, 볼트는 USDC가 아닌 준비금을 0으로 평가했으며, 공격자는 소량의 USDC를 입금하여 거의 전체 주식 공급량을 민팅한 후 실제 기반 자산으로 환매했습니다.
배경
dynBaseUSDCv3 볼트 (0x67b9...4dcd)는 여러 수익 창출 토큰을 보유하며 Uniswap V3를 통해 USDC가 아닌 준비금의 가격을 책정합니다. 가격 책정 도우미 getPrice(base, fee, quote, amount)는 팩토리를 통해 (base, quote, fee) 튜플을 Uniswap V3 풀로 해석한 후 해당 풀에서 TWAP을 읽습니다. 볼트의 totalAssets()는 가격이 책정된 준비금을 집계합니다. 주식 민팅 및 환매 비율은 이 합계에서 도출됩니다.
취약점 분석
결함은 getPrice()의 조기 반환 분기에 있습니다. IUniswapV3Factory.getPool(base, quote, fee)가 address(0)을 반환할 때(제공된 수수료 등급에 대한 풀이 존재하지 않음), 함수는 되돌리는 대신 0으로 초기화된 price 변수를 반환합니다. 볼트는 Uniswap V3의 지원되는 등급(500/3000/10000) 중 하나가 아닌 fee=42로 배포되었으므로, 모든 USDC가 아닌 토큰 조회가 이 분기에 해당됩니다. 따라서 totalAssets()는 볼트의 USDC 잔액만을 합산하며, 실제 수익 창출 토큰은 0을 기여합니다. totalAssets()에 의존하는 민팅 및 환매 비율은 이 거의 0에 가까운 분모를 기반으로 계산됩니다.

공격 분석
다음 분석은 트랜잭션 0x00b949...8d3732를 기반으로 합니다.
-
1단계: 공격자는 약 100K
USDC를 플래시론으로 조달했습니다. -
2단계: 공격자는
USDC를 볼트에 입금했습니다.totalAssets()가USDC잔액만 집계했기 때문에, 볼트는 자신을 대략 입금 금액으로 평가했고 공격자는 거의 100%의 주식 공급량을 받았습니다. -
3단계: 공격자는 주식을 환매했으며, 이는 주식 소유에 비례하여 기반 준비금을 분배합니다. 공격자는 볼트가 보유한 모든 수익 창출 토큰의 상당 부분을 받았습니다.
-
4단계: 공격자는 플래시론을 상환하고 탈취한 수익 창출 토큰을 이익으로 보유했습니다.
결론
두 가지 확인이 누락되었습니다. 배포 시 fee=42가 Uniswap V3의 지원되는 등급(500/3000/10000)에 대해 검증되지 않았습니다. getPrice()는 풀이 없을 때 되돌리는 대신 0을 반환했습니다. 두 수정 중 하나만으로도 충분합니다. 설정 시점에 오라클 매개변수를 검증하거나, getPool() == address(0)일 때 되돌립니다. 심층 방어로서, 주식 민팅 로직은 예치금을 수락하기 전에 totalAssets()를 외부 참조와 대조하여 건전성 검사를 해야 합니다.
Scallop
2026년 4월 26일, Sui의 Scallop 스테이킹 보상 프로그램이 약 $142.7K의 피해를 입었습니다. 사용자의 적립 보상을 업데이트하는 함수가 전달된 보상 추적 객체가 사용자 계정과 일치하는지 확인하지 않았습니다. 이로 인해 공격자는 오래되어 휴면 상태인 보상 추적 객체에서 허구의 포인트 잔액을 꺼내 잔액이 소진될 때까지 합법적인 보상 풀에 대해 환매할 수 있었습니다.
배경
Scallop은 Sui의 대출 프로토콜입니다. 대출 상품 위에 Scallop은 spool 프로그램을 운영합니다. 사용자는 단일 자산을 Scallop 시장에 입금하여 MarketCoin<T>(대출 영수증. SUI 입금의 경우 MarketCoin<SUI>, 온체인에서 "sSUI"의 표현)를 받은 후, 해당 MarketCoin을 Spool에 스테이킹하여 시간이 지남에 따라 프로토콜 포인트를 얻고, 나중에 페어된 RewardsPool에 대해 포인트를 환매하여 실제 보상 토큰을 받습니다. 각 Spool은 글로벌 주당 index를 추적하는 Sui 공유 객체입니다. 각 사용자는 스테이킹된 잔액과 적립된 points를 기록하는 개인 SpoolAccount를 보유합니다.
취약점 분석
결함은 spool::user::update_points에 있습니다. 이 함수는 account.spool_id == object::id(spool) (또는 account.stake_type == spool.stake_type)를 assert하지 않습니다. 형제 항목인 stake, unstake, redeem_rewards는 모두 진입 시 해당 바인딩 확인을 수행합니다. 오직 update_points만 이를 건너뜁니다. 확인이 없으면 spool_account::accrue_points는 전달된 모든 Spool에 대해 account.points += stake * (spool.index − account.index) / 1e9를 계산하며, 해당 index를 이 계정 자신의 보상 스트림인 것처럼 취급합니다.

이 경로가 악용 가능한 이유는 Sui가 공유 객체를 가비지 컬렉션하지 않기 때문입니다. stakes가 먼지로 줄어든 버려진 Scallop Spool은 보상 지분(기간당 증분 1e9 * reward / stakes)을 계속 적립하므로, 그 index는 시간이 지남에 따라 누적적으로 증가하여 임의로 큰 값에 도달할 수 있습니다. 바인딩 확인이 없으면 update_points는 이 부풀려진 index를 사용하여 어떤 계정에든 큰 포인트 델타를 기록할 수 있습니다. 오염된 points는 이후 대상 spool의 RewardsPool에 대해 1:1로 환매됩니다. 계정이 해당 대상 spool에 합법적으로 바인딩되어 있고 redeem_rewards 자체의 바인딩 확인이 통과되기 때문입니다.
공격 분석
다음 분석은 트랜잭션 6WNDjC...NfVL를 기반으로 합니다.
-
1단계: 미끼로 0.2
SUI를 사용하여 공격자는MarketCoin<SUI>를 민팅한 후,new_spool_account+stake를 대상 spool에 대해 호출하여account.spool_id = target_spool로 합법적으로 바인딩된SpoolAccount를 생성했습니다. -
2단계: 공격자는
donor_spool을 버려진Spool로 설정하여update_points<MarketCoin<SUI>>(donor_spool, account, clock)을 호출했습니다. 기증자의index(≈8.91e14)가 계정에points로 기록되었습니다:points = stake * (8.91e14 − 1.19e9) / 1e9 ≈ 1.62e14. -
3단계: 공격자는
redeem_rewards<MarketCoin<SUI>, SUI>(target_spool, target_rp, account)를 호출했습니다. 바인딩 assert가 대상 바인딩 계정을 수락했고, 내부 재적립이 조기 반환되었으며, 오염된points가 보상 풀의 잔액까지 1:1 비율로 변환되었습니다:rewards = 150,098,061,595,978원시SUI. -
4단계: 공격자는
unstake와redeem을 호출하여 0.2SUI미끼를 회수한 후,TransferObjects로 모든 것을 이동시켰습니다.
결론
수정 방법은 stake, unstake, redeem_rewards가 이미 수행하는 것처럼 update_points 진입 시 동일한 assert!(account.spool_id == object::id(spool)) 확인을 추가하는 것입니다. 심층 방어로서, 프로토콜은 단일 accrue_points 호출이 수락하는 index 델타에 상한을 두어(설정된 상한보다 큰 델타 거부), 향후 바인딩 확인이 다시 우회되더라도 단일 호출이 실제 스테이킹 기간에 비해 불균형적인 points 양을 계정에 적립할 수 없도록 할 수 있습니다.
BlockSec 소개
BlockSec은 풀스택 블록체인 보안 및 암호화폐 컴플라이언스 제공업체입니다. 저희는 고객이 코드 감사(스마트 컨트랙트, 블록체인, 지갑 포함)를 수행하고, 실시간으로 공격을 차단하며, 사고를 분석하고, 불법 자금을 추적하며, 프로토콜 및 플랫폼의 전체 생애주기에 걸쳐 AML/CFT 의무를 이행할 수 있도록 돕는 제품과 서비스를 구축합니다.
BlockSec은 권위 있는 학회에서 다수의 블록체인 보안 논문을 발표했으며, DeFi 애플리케이션의 여러 제로데이 공격을 보고하고, 여러 해킹을 차단하여 2천만 달러 이상을 구제했으며, 수십억 달러 규모의 암호화폐를 보호했습니다.
-
공식 웹사이트: https://blocksec.com/
-
공식 트위터 계정: https://twitter.com/BlockSecTeam



