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