code4rena-2024-03-dittoeth-m02

[M-02] Can manipulate the C.SHORT_STARTING_ID ShortRecord of the TAPP

보고서

Summary

숏 ID 재활용 리스트에 특수한 목적으로 예약된 ID를 넣을 수 있고, NFT화 된 숏을 이동하여 재활용 리스트의 ID를 소비, 특수 목적 숏의 데이터를 덮어쓸 수 있다.

Keyword

logic flaw, nft, defi

Vulnerability

TAPP(특수 주소, 다이아몬드 프록시 주소인 address(this) 이용)은 일반적으로 다른 숏은 가지지 않고, C.SHORT_STARTING_ID(첫번째 id) 숏을 변제를 위하여 특수하게 사용한다. 다른 숏의 변제 과정 중 변제되지 못한 숏은 TAPP 소유의 C.SHORT_STARTING_ID id 숏으로 흡수된다. 추후 수수료를 통해 dETH를 확보하면 TAPP 소유의 C.SHORT_STARTING_ID 숏을 대상으로 PrimaryLiquidationFacet.liquidate 를 호출하여 프로토콜 빚을 줄여간다. TAPP 숏의 모든 ercDebt(dUSD)가 변제되면 LibShortRecord.deleteShortRecord 를 호출하여 해당 숏을 닫고 ID를 재활용 리스트로 보낸다.

function _fullorPartialLiquidation(MTypes.PrimaryLiquidation memory m) private {
    STypes.VaultUser storage TAPP = s.vaultUser[m.vault][address(this)];
    uint88 decreaseCol = min88(m.totalFee + m.ethFilled, m.short.collateral);
 
@>  if (m.short.ercDebt == m.ercDebtMatched) { // 전부 매칭된 경우
        // Full liquidation
        LibSRUtil.disburseCollateral(m.asset, m.shorter, m.short.collateral, m.short.dethYieldRate, m.short.updatedAt);
@>      LibShortRecord.deleteShortRecord(m.asset, m.shorter, m.short.id); // 숏을 닫고 ID를 재활용 리스트로 보내
        ...
    } else { // 부분 매칭된 경우
        ...
    }
    ...
}

이후 다른 유저가 변제를 요청했을 때 부분 매칭되었고 담보 손실이 일어났다면 남은 숏은 TAPP의 C.SHORT_STARTING_ID 숏에 흡수된다. 이때 C.SHORT_STARTING_ID 숏은 다시 SR.FullyFilled 상태로 변경된다. 하지만 TAPP의 숏 ID 재활용 리스트에 있는 C.SHORT_STARTING_ID는 다시 활성 리스트로 이동하지 않는다.

function _fullorPartialLiquidation(MTypes.PrimaryLiquidation memory m) private {
    STypes.VaultUser storage TAPP = s.vaultUser[m.vault][address(this)];
    uint88 decreaseCol = min88(m.totalFee + m.ethFilled, m.short.collateral);
 
    if (m.short.ercDebt == m.ercDebtMatched) { // 전부 매칭된 경우
        ...
@>  } else { // 부분 매칭된 경우
        ...
        // TAPP absorbs leftover short, unless it already owns the short
        if (m.loseCollateral && m.shorter != address(this)) {
            // Delete partially liquidated short
            LibShortRecord.deleteShortRecord(m.asset, m.shorter, m.short.id);
            // Absorb leftovers into TAPP short
@>          LibShortRecord.fillShortRecord(
                m.asset,
@>              address(this),
@>              C.SHORT_STARTING_ID,
                SR.FullyFilled,
                m.short.collateral,
                m.short.ercDebt,
                s.asset[m.asset].ercDebtRate,
                m.short.dethYieldRate
            );
        }
    }
 
    ...
}

여기서 간과한 것은 TAPP이 C.SHORT_STARTING_ID id 의 특수 숏만을 가진다고 가정한 것, 따라서 TAPP 의 숏 ID 재활용 리스트를 고려하지 않은 것이다.

유저는 자신의 숏을 NFT화 시켜 TAPP 에게 보내 TAPP이 숏을 추가로 가지게 할 수 있다. NFT화 된 숏을 TAPP에게 보내면 TAPP의 숏 ID 재활용 리스트에 있는 ID를 먼저 사용한다. 만약 C.SHORT_STARTING_ID 숏이 삭제된 상태, 즉 재활용 리스트에 C.SHORT_STARTING_ID 가 있는 상태에 NFT화 된 숏을 TAPP에게 보내면 C.SHORT_STARTING_ID id를 가진 숏이 생성되고 기존에 C.SHORT_STARTING_ID 숏에 기록되어 있던 데이터를 덮어쓰게 된다.

function transferShortRecord(address from, address to, uint40 tokenId) internal {
    ...
 
@>  uint8 id = LibShortRecord.createShortRecord(
        asset, to, SR.FullyFilled, short.collateral, short.ercDebt, short.ercDebtRate, short.dethYieldRate, tokenId
    );
 
    nft.owner = to;
    nft.shortRecordId = id;
    nft.shortOrderId = 0;
}

Impact

TAPP에 모인 빚을 처분할 수 없도록 하여 장기적으로 빚이 누적되게 할 수 있다.

Mitigation

NFT화된 숏을 TAPP에게 전달할 수 없도록 막는다.


tags: bughunting, dittoeth, smart contract, solidity, logic flaw, nft, defi, solo issue, severity medium