code4rena-2024-03-dittoeth-m08
[M-08] If a redemption has N disputable shorts, it is possible to dispute N-1 times the redemption to maximize the penalty
Summary
이의제기를 할 때, 담보율을 확인할 인덱스를 유저가 제공한다. 인덱스에 따라 이의제기를 쪼개서 할 수 있는데, 쪼개서 했을 때와 처음부터 정확한 인덱스에 했을 때의 패널티 양이 다르다.
Keyword
logic flaw, lack of input validation
Vulnerability
청산 요청을 했을 때 청산 대상이지만 청산 요청 되지 않은 숏이 있다면 disputeRedemption 를 호출하여 이의를 제기할 수 있다. 반대에 성공하면 이의제기에 사용된 숏보다 담보율이 높은 숏에 대한 청산 요청은 해제되고, 청산 요청자는 패널티를 받게 된다. 이의제기자는 패널티 만큼을 콜 수수료로 받아간다.
패널티 비율은 penaltyPct = min(max(callerFeePct, (currentProposal.CR - disputeCR) / currentProposal.CR), 0.33 ether) 로 계산된다. 즉 최대 33%, 최소 설정값(callerFeePct, 여기서는 0.5% 라 하자) 사이이고, 청산 대상 중 담보율이 가장 높은 숏과 이의제기 숏의 담보율 차이가 높을 수록 높아진다. 패널티의 양은 penaltyAmt = incorrectErcDebt * penaltyPct 로 청산 요청이 해제되는 숏의 dUSD의 penaltyPct % 만큼을 받는다.
disputeRedemption의 incorrectIndex 인덱스부터 시작하여 청산 요청 배열을 확인한다. 문제는 예를들어 3번 인덱스부터 담보율이 더 높을 때, 3번 인덱스보다 높은 인덱스부터 이의를 제기하여 여러번에 걸쳐 disputeRedemption 를 호출할 때 발생한다.
function disputeRedemption(address asset, address redeemer, uint8 incorrectIndex, address disputeShorter, uint8 disputeShortId)
external
isNotFrozen(asset)
nonReentrant
{
...
@> MTypes.ProposalData memory incorrectProposal = decodedProposalData[incorrectIndex];
MTypes.ProposalData memory currentProposal;
STypes.Asset storage Asset = s.asset[d.asset];
uint256 disputeCR = disputeSR.getCollateralRatio(redeemerAssetUser.oraclePrice);
@> if (disputeCR < incorrectProposal.CR && disputeSR.updatedAt + C.DISPUTE_REDEMPTION_BUFFER <= redeemerAssetUser.timeProposed)
{
// @dev All proposals from the incorrectIndex onward will be removed
// @dev Thus the proposer can only redeem a portion of their original slate
@> for (uint256 i = incorrectIndex; i < decodedProposalData.length; i++) {
@> currentProposal = decodedProposalData[i];
STypes.ShortRecord storage currentSR = s.shortRecords[d.asset][currentProposal.shorter][currentProposal.shortId];
currentSR.collateral += currentProposal.colRedeemed;
currentSR.ercDebt += currentProposal.ercDebtRedeemed;
@> d.incorrectCollateral += currentProposal.colRedeemed;
d.incorrectErcDebt += currentProposal.ercDebtRedeemed;
}
...
// @dev Penalty is based on the proposal with highest CR (decodedProposalData is sorted)
// @dev PenaltyPct is bound between CallerFeePct and 33% to prevent exploiting primaryLiquidation fees
@> uint256 penaltyPct = LibOrders.min(
LibOrders.max(LibAsset.callerFeePct(d.asset), (currentProposal.CR - disputeCR).div(currentProposal.CR)), 0.33 ether
);
@> uint88 penaltyAmt = d.incorrectErcDebt.mulU88(penaltyPct);
// @dev Give redeemer back ercEscrowed that is no longer used to redeem (penalty applied)
@> redeemerAssetUser.ercEscrowed += (d.incorrectErcDebt - penaltyAmt);
@> s.assetUser[d.asset][msg.sender].ercEscrowed += penaltyAmt;
} else {
revert Errors.InvalidRedemptionDispute();
}
}다음 상황을 생각해보자.
- 4개의 숏이 있고, 담보율(CR)은 CR1 < CR2 < CR3 < CR4 이다.
- CR1 = 1.1, CR2 = 1.2, CR3 = 1.3, CR4 = 1.4
- 모두
ercDebt는 1ETH 이라고 하자.
- 청산 요청자가 [Short2, Short3, Short4] 를 청산 요청했다.
- Short1은 이들 모두보다 담보율이 낮다.
- CR1 < CR2 이기 때문에 청산 요청된 첫번째 숏부터 틀렸다. 하지만 공격자(이의제기자)는 인덱스 2 부터 이의제기를 0, 1, 2 총 3번
disputeRedemption를 호출할 수 있다.
처음부터 0번 인덱스를 이의제기에 사용했다면 penaltyPct = min( max(0.005, (1.4 - 1.1)/1.4 ), 0.33) = min( max(0.005, 0.214), 0.33) = 0.214 이고 (1 + 1 + 1)ETH * 0.214 = 0.642 ETH 만큼이 패널티가 된다.
3번에 걸쳐 이의제기를 했다면 다음과 같다.
penaltyPct = min( max(0.005, (1.4 - 1.1)/1.4), 0.33) = min( max(0.005, 0.214), 0.33) = 0.214penaltyAmt = 1 ETH * 0.214 = 0.214 ETH
penaltyPct = min( max(0.005, (1.3 - 1.1)/1.3 ), 0.33) = min( max(0.005, 0.153), 0.33) = 0.153penaltyAmt = 1 ETH * 0.153 = 0.153 ETH
penaltyPct = min( max(0.005, (1.2 - 1.1)/1.2 ), 0.33) = min( max(0.005, 0.083), 0.33) = 0.083penaltyAmt = 1 ETH * 0.083 = 0.083 ETH
- 패널티의 총 합은 0.45 ETH로, 0번 인덱스로 바로 이의제기 했을 때보다 작다.
Impact
이의제기를 쪼개서 했을 때와 처음부터 정확한 인덱스에 했을 때의 패널티 양이 다르다.
Mitigation
incorrectIndex 가 n 일 때, n-1 인덱스의 담보율이 이의제기 숏의 담보율보다 낮은지 확인한다.
Memo
원본 보고서는 예시에서 계산을 잘못했다. 처음부터 잘 호출했을 때(0번 인덱스로 바로 이의제기시) 패널티가 더 높다. 따라서 공격자(이의제기자)가 이득을 보지 못한다. 청산 요청자가 잘못됨을 깨닫고 자신의 패널티를 줄이기 위해 이런 짓을 한다는 상황도 올바르지 않다. 그냥 청산자의 다른 계정을 이용하면 패널티를 콜 수수료로 패널티만큼을 받기 때문이다.
다만 쪼개서 했을 때와 그냥 했을 때의 패널티에 차이가 있다는 점은 버그라고 할 만 하다.
tags: bughunting, dittoeth, smart contract, solidity, logic flaw, lack-of-input-validation-vul, severity medium