code4rena-2024-03-dittoeth-m07

[M-07] Using cached price to create a proposal reduce the efficacity of redemptions for asset peg

보고서

Summary

캐시한 오라클 데이터가 최신 데이터인지 확인하지 않고 사용한다. 잘못된 가격을 기반으로 청산 대상을 판단하므로 청산 대상이 아닌 숏을 청산시킬 수 있다.

Keyword

price oracle, staled oracle

Vulnerability

proposeRedemption 에서는 캐시된 오라클 가격을 사용하여 담보 비율을 계산한다. 이 때 캐시된 가격 정보가 오래된 것인지 확인하지 않고 사용한다. 잘못된 가격을 사용하여 담보를 계산한다면 실제로는 청산 대상이 아닌 숏을 청산시킬 수 있다.

function proposeRedemption(
    address asset,
    MTypes.ProposalInput[] calldata proposalInput,
    uint88 redemptionAmount,
    uint88 maxRedemptionFee
) external isNotFrozen(asset) nonReentrant {
    if (proposalInput.length > type(uint8).max) revert Errors.TooManyProposals();
    MTypes.ProposeRedemption memory p;
    p.asset = asset;
    STypes.AssetUser storage redeemerAssetUser = s.assetUser[p.asset][msg.sender];
    uint256 minShortErc = LibAsset.minShortErc(p.asset);
 
    if (redemptionAmount < minShortErc) revert Errors.RedemptionUnderMinShortErc();
 
    if (redeemerAssetUser.ercEscrowed < redemptionAmount) revert Errors.InsufficientERCEscrowed();
 
    // @dev redeemerAssetUser.SSTORE2Pointer gets reset to address(0) after actual redemption
    if (redeemerAssetUser.SSTORE2Pointer != address(0)) revert Errors.ExistingProposedRedemptions();
 
@>  p.oraclePrice = LibOracle.getPrice(p.asset); // no price update before use this cached price
 
    bytes memory slate;
    for (uint8 i = 0; i < proposalInput.length; i++) {
        ...
@>      p.currentCR = currentSR.getCollateralRatio(p.oraclePrice);
 
        ...
@>      p.colRedeemed = p.oraclePrice.mulU88(p.amountProposed);
        if (p.colRedeemed > currentSR.collateral) {
            p.colRedeemed = currentSR.collateral;
        }
        ...
    }
    ...
}

Impact

잘못된 가격을 사용하여 담보를 계산한다면 실제로는 청산 대상이 아닌 숏을 청산시킬 수 있다.

Mitigation

캐시가 오래 되면 다시 오라클에 쿼리하여 캐시를 업데이트 하는 getSavedOrSpotOraclePrice 함수를 이용한다.

function proposeRedemption(
    address asset,
    MTypes.ProposalInput[] calldata proposalInput,
    uint88 redemptionAmount,
    uint88 maxRedemptionFee
) external isNotFrozen(asset) nonReentrant {
    if (proposalInput.length > type(uint8).max) revert Errors.TooManyProposals();
    MTypes.ProposeRedemption memory p;
    p.asset = asset;
    STypes.AssetUser storage redeemerAssetUser = s.assetUser[p.asset][msg.sender];
    uint256 minShortErc = LibAsset.minShortErc(p.asset);
 
    if (redemptionAmount < minShortErc) revert Errors.RedemptionUnderMinShortErc();
 
    if (redeemerAssetUser.ercEscrowed < redemptionAmount) revert Errors.InsufficientERCEscrowed();
 
    // @dev redeemerAssetUser.SSTORE2Pointer gets reset to address(0) after actual redemption
    if (redeemerAssetUser.SSTORE2Pointer != address(0)) revert Errors.ExistingProposedRedemptions();
 
-    p.oraclePrice = LibOracle.getPrice(p.asset);
+    p.oraclePrice = LibOracle.getSavedOrSpotOraclePrice(p.asset);
    ...
}

tags: bughunting, dittoeth, smart contract, solidity, price oracle, staled oracle, severity medium