슬로우 미스트 분석 크림 1억 3천만 달러 도난 사건: 대출 풀 결함을 이용한 악의적인 가격 조작
作者:Kong,느린 안개 보안 팀
느린 안개 소식에 따르면, 2021년 10월 27일, Cream Finance가 다시 공격을 받아 약 1억 3천만 달러의 손실을 입었으며, 느린 안개 보안 팀이 즉시 분석에 착수하였고, 간략한 분석을 아래와 같이 공유합니다.
공격 핵심
이번 공격의 핵심은 Cream 대출 풀의 담보물 가격 획득의 결함을 이용하여, 악의적으로 담보물의 가격을 끌어올려 공격자가 Cream 대출 풀에서 더 많은 토큰을 빌릴 수 있도록 한 것입니다.
공격 세부사항
먼저 공격자는 DssFlash에서 5억 개의 DAI를 플래시 론으로 빌린 후, 빌린 5억 개의 DAI를 yearn의 yDAI 풀에 담보로 제공하여 약 4.5억 개의 yDAI 증서를 얻었습니다.
그 후 공격자는 얻은 yDAI 토큰을 Curve의 yDAI/yUSDC/yUSDT/yTUSD 풀에서 단일 자산 유동성 추가를 통해 해당 유동성 증서를 얻었습니다. 이어서 공격자는 얻은 증서를 yvWBTC 풀에 담보로 제공하여 yUSD 증서를 얻고, 이후 Cream crYUSD 대출 풀에 담보로 제공할 준비를 했습니다.
그 후 공격자는 Cream의 crYUSD 대출 풀에 yUSD 증서를 담보로 제공하기 시작했습니다. 담보 규모를 확대하기 위해 공격자는 AAVE에서 약 52.4만 개의 WETH를 플래시 론으로 빌리고, 이를 Cream의 crETH 풀에 담보로 제공했습니다.
공격자는 crETH 풀에 대량의 ETH를 담보로 제공하여 crYUSD 풀의 yUSD를 모두 빌릴 수 있는 충분한 대출 능력을 확보하고, 이를 crYUSD 풀에 반복적으로 담보로 제공했습니다. 이어서 crYUSD 풀에서 순환 대출을 통해 레버리지 형태로 crYUSD 풀에서 yUSD의 담보 규모를 확대하여 후속 가격 조작을 위한 준비를 했습니다.
그 후 공격자는 가격 조작을 위해 yDAI/yUSDC/yUSDT/yTUSD 4Pool 증서를 얻기 위해 약 1,873개의 ETH를 Uniswap V3에서 약 745만 개의 USDC로 교환하고, Curve 3Pool을 통해 이를 DUSD 토큰 약 338만 개로 교환했습니다.
다음으로 공격자는 얻은 DUSD 토큰을 통해 YVaultPeak에서 yDAI/yUSDC/yUSDT/yTUSD 4Pool 증서를 상환하고, 이 증서를 이용해 yUSD(yvWBTC) 풀에서 yDAI/yUSDC/yUSDT/yTUSD 토큰을 인출했습니다.
그 후 공격자는 이번 공격의 핵심 작업을 시작했습니다. 약 843만 개의 yDAI/yUSDC/yUSDT/yTUSD 토큰을 yUSD 풀로 직접 전송했습니다. 이는 정상적인 담보 작업을 통해 담보로 제공되지 않았기 때문에, 이 843만 개의 yDAI/yUSDC/yUSDT/yTUSD 토큰은 별도로 기록되지 않고, yDAI/yUSDC/yUSDT/yTUSD 증서의 보유자에게 직접 분산되었습니다. 이는 해당 share의 가격을 직접 끌어올리는 것과 같습니다.
crToken에서 담보물 가격이 악의적으로 끌어올려졌기 때문에, 공격자가 담보로 제공한 대량의 yUSD는 더 많은 자금을 빌릴 수 있게 해주었습니다. 결국 공격자는 Cream의 다른 15개 풀을 모두 비웠습니다. 다음으로 우리는 Cream의 crToken 대출 풀에서 구체적인 대출 논리를 살펴보겠습니다.
cToken 계약에서 우리는 주요 대출 검사가 borrowAllowed 함수에서 이루어짐을 확인할 수 있습니다:
borrowAllowed 함수를 따라가면, 427행에서 getHypotheticalAccountLiquidityInternal 함수를 통해 실시간 상태에서 해당 계좌에 해당하는 모든 cToken의 자산 가치 총합과 대출의 자산 가치 총합을 확인하고, cToken의 자산 가치와 대출의 Token 가치 총합을 비교하여 사용자가 계속 대출할 수 있는지를 판단합니다.
getHypotheticalAccountLiquidityInternal 함수를 따라가면, 담보물의 가치 획득이 886행의 oracle.getUnderlyingPrice에서 이루어짐을 발견할 수 있습니다.
예언자의 getUnderlyingPrice 함수를 따라가면, 150행의 getYvTokenPrice 함수를 통해 가격을 획득하는 것을 쉽게 발견할 수 있습니다.
getYvTokenPrice 함수를 계속 따라가면, yvTokenInfo.version이 V2이므로 yVault의 pricePerShare 함수를 통해 가격을 획득합니다.
pricePerShare를 따라가면, _shareValue를 가격으로 직접 반환하는 것을 발견할 수 있으며, _shareValue는 _totalAssets를 계약의 총 share 수량 (self.totalSupply)로 나누어 단일 share의 가격을 계산합니다. 따라서 공격자는 _totalAssets를 조작하여 끌어올리기만 하면 단일 share의 가격을 높여 공격자의 담보물 가치를 증가시켜 더 많은 다른 토큰을 빌릴 수 있습니다.
우리는 _totalAssets가 어떻게 획득되는지를 살펴볼 수 있습니다. 772행에서 우리는 _totalAssets가 현재 계약의 yDAI/yUSDC/yUSDT/yTUSD 토큰 수량과 전략 풀에 담보된 자산 수치를 더하여 얻어짐을 명확히 볼 수 있습니다. 따라서 공격자는 yUSD 계약에 yDAI/yUSDC/yUSDT/yTUSD 토큰을 직접 전송하여 share 가격을 끌어올려 이익을 얻을 수 있습니다.
EthTx.info를 통해 pricePerShare의 전후 변화를 명확히 볼 수 있습니다:
마지막으로 공격자는 다른 풀을 비운 후 플래시 론을 상환하고 이익을 챙겼습니다.
요약
이번 공격은 전형적인 플래시 론을 이용한 가격 조작입니다. Cream의 대출 풀이 yUSD 풀의 share 가격을 획득할 때 pricePerShare 인터페이스를 직접 사용하였고, 이 인터페이스는 계약의 담보물 잔액과 전략 풀의 담보 자산 수치를 더하여 총 share 수로 나누어 단일 share의 가격을 계산합니다. 따라서 사용자가 yUSD에 담보물을 직접 전송하면 쉽게 단일 share 가격을 끌어올릴 수 있으며, 결국 Cream 대출 풀에서 담보물을 통해 더 많은 자금을 빌릴 수 있게 됩니다.
부록: 이전 두 번의 Cream Finance 해킹 분석 회고