sherlock-2023-11-nouns-builder-h02
[H-02] Adversary can permanently brick auctions due to precision error in Auction._computeTotalRewards
Summary
정밀도 손실로 인해 totalRewards와 실제 리워드의 합이 일치하지 않는 상황이 발생할 수 있다. ProtocolRewards.depositRewards 에서는 이 두 값이 일치하지 않으면 트랜잭션을 취소시키며 이로 인해 영원히 경매를 낙찰/다음 경매를 진행할 수 없게 된다. 경매 기능이 영원히 중단된다.
Keyword
dos, arithmetic error, loss of precision
Vulnerability
경매 낙찰금은 추천자, DAO 창립자, Nouns builder 측에게 리워드로 주고, 남은 금액은 Treasury에 예치하여 DAO 운영 비용으로 사용한다.
_computeTotalRewards 에서 각자에게 분배할 리워드를 계산하고, ProtocolRewards.depositRewards 를 호출하여 ProtocolRewards 컨트랙트에 리워드 토큰(ETH)과 리워드 정보를 등록한다.
function _settleAuction() private {
...
// If a bid was placed:
if (_auction.highestBidder != address(0)) {
// Cache the amount of the highest bid
uint256 highestBid = _auction.highestBid;
// If the highest bid included ETH: Pay rewards and transfer remaining amount to the DAO treasury
if (highestBid != 0) {
// Calculate rewards
@> RewardSplits memory split = _computeTotalRewards(currentBidReferral, highestBid, founderReward.percentBps);
if (split.totalRewards != 0) {
// Deposit rewards
@> rewardsManager.depositBatch{ value: split.totalRewards }(split.recipients, split.amounts, split.reasons, "");
}
// Deposit remaining amount to treasury
_handleOutgoingTransfer(settings.treasury, highestBid - split.totalRewards);
}ProtocolRewards 컨트랙트는 오딧팅 스코프가 아니고, 또한 오딧팅 레포지토리에는 MockProtocolRewards만 있다(실제 코드와 비슷하긴 하다). 하지만 Nouns DAO의 공개된 레포지토리에서 실제 코드를 찾을 수 있다. ProtocolRewards.depositRewards 에서는 각자에게 분배하는 토큰의 합이 msg.value와 같지 않다면 트랜잭션을 취소시킨다.
function depositBatch(address[] calldata recipients, uint256[] calldata amounts, bytes4[] calldata reasons, string calldata comment) external payable {
uint256 numRecipients = recipients.length;
if (numRecipients != amounts.length || numRecipients != reasons.length) {
revert ARRAY_LENGTH_MISMATCH();
}
uint256 expectedTotalValue;
for (uint256 i; i < numRecipients; ) {
expectedTotalValue += amounts[i];
unchecked {
++i;
}
}
@> if (msg.value != expectedTotalValue) {
revert INVALID_DEPOSIT();
}
...따라서 _computeTotalRewards 함수에서 정밀도 손실이 발생한다면 단 1 wei 차이로도 ProtocolRewards.depositRewards 함수가 실패하고, 경매 정산이 안 되면 다음 경매로 넘어갈 수 없어 경매 기능이 영구적으로 중단된다.
_computeTotalRewards는 각 수신자에게 분배된 퍼센테이지만큼을 계산하여 할당한다. 백분율의 총 합만큼을 곱한 (_finalBidAmount * totalBPS) / BPS_PER_100_PERCENT 를 totalRewards로 계산한다. 따라서 나눗셈 계산 중 정밀도 손실이 발생할 수 있다.
function _computeTotalRewards(
address _currentBidRefferal,
uint256 _finalBidAmount,
uint256 _founderRewardBps
) internal view returns (RewardSplits memory split) {
// Get global builder recipient from manager
address builderRecipient = manager.builderRewardsRecipient();
// Calculate the total rewards percentage
uint256 totalBPS = _founderRewardBps + referralRewardsBPS + builderRewardsBPS;
// Verify percentage is not more than 100
if (totalBPS >= BPS_PER_100_PERCENT) {
revert INVALID_REWARD_TOTAL();
}
// Calulate total rewards
@> split.totalRewards = (_finalBidAmount * totalBPS) / BPS_PER_100_PERCENT;
// Check if founder reward is enabled
bool hasFounderReward = _founderRewardBps > 0 && founderReward.recipient != address(0);
// Set array size based on if founder reward is enabled
uint256 arraySize = hasFounderReward ? 3 : 2;
// Initialize arrays
split.recipients = new address[](arraySize);
split.amounts = new uint256[](arraySize);
split.reasons = new bytes4[](arraySize);
// Set builder reward
split.recipients[0] = builderRecipient;
@> split.amounts[0] = (_finalBidAmount * builderRewardsBPS) / BPS_PER_100_PERCENT;
// Set referral reward
split.recipients[1] = _currentBidRefferal != address(0) ? _currentBidRefferal : builderRecipient;
@> split.amounts[1] = (_finalBidAmount * referralRewardsBPS) / BPS_PER_100_PERCENT;
// Set founder reward if enabled
if (hasFounderReward) {
split.recipients[2] = founderReward.recipient;
@> split.amounts[2] = (_finalBidAmount * _founderRewardBps) / BPS_PER_100_PERCENT;
}
}추천 보상이 5%(500)이고 빌더 보상이 5%(500)로 총 10%(1000)라고 가정해보자. 공격자는 컨트랙트를 중단시키기 위해 특정 마지막 숫자로 입찰가를 조작할 수 있다. 끝자리가 19로 끝나는 입찰을 예로 들어보면 totalRewards는 1이지만 각자에게 분배된 리워드는 0으로 정밀도 손실이 발생한다.
split.totalRewards = (19 * 1,000) / 100,000 = 190,000 / 100,000 = 1
split.amounts[0] = (19 * 500) / 100,000 = 95,000 / 100,000 = 0
split.amounts[1] = (19 * 500) / 100,000 = 95,000 / 100,000 = 0Impact
경매 기능이 영구히 중단된다.
Mitigation
totalRewards를 백분율의 합계로 계산하는 대신 계산된 각 수수료의 합으로 계산한다. 이렇게 하면 어떤 경우에도 항상 일치한다.
tags: bughunting, nouns dao, smart contract, solidity, dos, arithmetic error, loss of precision, severity high