Compound V2는 Lending protocol에서 핵심적인 기능을 간결하게 구현하였고 오랜 기간 서비스를 유지해오면서 보안성을 인정받아 여러 protocol에서 자주 fork하여 사용하는 사례가 많습니다.
그러나 Compound V2에서 취약점이 발견될 경우 fork된 여러 프로토콜들은 공격 대상이 될 수 있습니다.
잘 알려진 취약점이지만 꾸준히 사고가 발생합니다.
그래서 어떠한 문제 때문에 발생하는지 분석을 진행해보았습니다.
function getHypotheticalAccountLiquidityInternal(
address account,
CToken cTokenModify,
uint redeemTokens,
uint borrowAmount) internal view returns (Error, uint, uint) {
AccountLiquidityLocalVars memory vars; // Holds all our calculation results
uint oErr;
// For each asset the account is in
CToken[] memory assets = accountAssets[account];
for (uint i = 0; i < assets.length; i++) {
CToken asset = assets[i];
// Read the balances and exchange rate from the cToken
(oErr, vars.cTokenBalance, vars.borrowBalance, vars.exchangeRateMantissa) = asset.getAccountSnapshot(account);
if (oErr != 0) { // semi-opaque error code, we assume NO_ERROR == 0 is invariant between upgrades
return (Error.SNAPSHOT_ERROR, 0, 0);
}
vars.collateralFactor = Exp({mantissa: markets[address(asset)].collateralFactorMantissa});
vars.exchangeRate = Exp({mantissa: vars.exchangeRateMantissa});
// Get the normalized price of the asset
vars.oraclePriceMantissa = oracle.getUnderlyingPrice(asset);
if (vars.oraclePriceMantissa == 0) {
return (Error.PRICE_ERROR, 0, 0);
}
vars.oraclePrice = Exp({mantissa: vars.oraclePriceMantissa});
// Pre-compute a conversion factor from tokens -> ether (normalized price value)
vars.tokensToDenom = mul_(mul_(vars.collateralFactor, vars.exchangeRate), vars.oraclePrice);
// sumCollateral += tokensToDenom * cTokenBalance
vars.sumCollateral = mul_ScalarTruncateAddUInt(vars.tokensToDenom, vars.cTokenBalance, vars.sumCollateral);
// sumBorrowPlusEffects += oraclePrice * borrowBalance
vars.sumBorrowPlusEffects = mul_ScalarTruncateAddUInt(vars.oraclePrice, vars.borrowBalance, vars.sumBorrowPlusEffects);
// Calculate effects of interacting with cTokenModify
if (asset == cTokenModify) {
// redeem effect
// sumBorrowPlusEffects += tokensToDenom * redeemTokens
vars.sumBorrowPlusEffects = mul_ScalarTruncateAddUInt(vars.tokensToDenom, redeemTokens, vars.sumBorrowPlusEffects);
// borrow effect
// sumBorrowPlusEffects += oraclePrice * borrowAmount
vars.sumBorrowPlusEffects = mul_ScalarTruncateAddUInt(vars.oraclePrice, borrowAmount, vars.sumBorrowPlusEffects);
}
}
// These are safe, as the underflow condition is checked first
if (vars.sumCollateral > vars.sumBorrowPlusEffects) {
return (Error.NO_ERROR, vars.sumCollateral - vars.sumBorrowPlusEffects, 0);
} else {
return (Error.NO_ERROR, 0, vars.sumBorrowPlusEffects - vars.sumCollateral);
}
}
- 사용자가 토큰을 빌리거나 인출을 진행할 때 getHypotheticalAccountLiquidityInternal 함수를 호출하여 담보와 대출 상황을 계산하고 사용자의 대출 가능한 유동성을 확인 후 담보가치가 더 큰 경우에만 정상적으로 진행됩니다.
- 사용자의 담보 가치를 계산할 때 환율이 포함됩니다.
function exchangeRateStoredInternal() virtual internal view returns (uint) {
uint _totalSupply = totalSupply;
if (_totalSupply == 0) {
/*
* If there are no tokens minted:
* exchangeRate = initialExchangeRate
*/
return initialExchangeRateMantissa;
} else {
/*
* Otherwise:
* exchangeRate = (totalCash + totalBorrows - totalReserves) / totalSupply
*/
uint totalCash = getCashPrior();
uint cashPlusBorrowsMinusReserves = totalCash + totalBorrows - totalReserves;
uint exchangeRate = cashPlusBorrowsMinusReserves * expScale / _totalSupply;
return exchangeRate;
}
}
환율은 마켓이 가지고 있는 총 잔액 + 총 차입금액 - 준비금 / 총 LP 토큰량으로 계산됩니다.
만약 마켓의 totalSupply 값이 0인 경우에 공격자는 다음과 같은 과정을 통해서 환율을 조작할 수 있습니다.
- 소량의 토큰을 예치(mint)하고 인출(redeem)하여 totalSupply를 원하는 값으로 조작
- 많은 량의 토큰을 기부(transfer)하여 totalCash 값을 증가
결과적으로 공격자의 담보가 비정상적으로 크게 측정되게 됩니다.
다음은 실제 공격한 tx를 바탕으로 조작한 환율을 통해 어떠한 방식으로 공격을 진행하였는지 알아보겠습니다.
- 공격자가 totalSupply 0보다 크게 조작하고
- transfer을 통해서 해당 마켓의 totalCash 값을 증가시켜 환율을 조작합니다.
조작된 환율을 통해 부풀려진 담보 가치를 이용해서 다른 마켓의 토큰들을 빌리고
donation attack에 사용되었던 토큰을 회수합니다.
이때 회수에 필요한 LP 토큰은 1개입니다.
- 소수점이 누락되는 rounding issue가 존재 (redeemAmount * 1e18 / exchangeRate)
이러한 흐름으로 공격이 진행되며 작은 금액으로 마켓의 토큰을 탈취하여 Lending protocol을 파산시킬 수 있는 취약점입니다.
따라서 새로운 마켓을 만들 때 초기 Collateral Factor 값을 잠시 0으로 설정하여 공격을 방지해야합니다.
Ref
- https://x.com/hackenclub/status/1791027653316633018
- https://x.com/MetaSec_xyz/status/1742220506890436991
- https://x.com/peckshield/status/1647307128267476992
'Analyze' 카테고리의 다른 글
Bybit 해킹 (3) | 2025.03.08 |
---|---|
트랜잭션 분석을 통해 계정 추상화 알아보기 (0) | 2025.01.18 |