Skip to content

Latest commit

 

History

History
102 lines (77 loc) · 5.02 KB

File metadata and controls

102 lines (77 loc) · 5.02 KB

Nice Pearl Canary

Medium

Unbounded Loop in buyVotes Function Leading to Potential Denial-of-Service

Summary and Impact

https://github.com/sherlock-audit/2024-12-ethos-update/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L440-L497

The buyVotes function within the ReputationMarket smart contract contains an unbounded loop that can be exploited to cause a Denial-of-Service (DoS) condition. Specifically, when a user attempts to purchase an excessively large number of votes (maxVotesToBuy) with insufficient ETH (msg.value), the function enters a while-loop that decrements currentVotesToBuy iteratively until the cost of purchasing the votes is less than or equal to the sent ETH. This linear decrement approach does not impose a strict upper limit on the number of iterations, potentially leading to transactions that consume excessive gas and ultimately revert.

Impact:

  • User Experience Degradation: Users attempting legitimate large purchases may encounter failed transactions due to gas exhaustion.
  • System Reliability: Repeated failed transactions can disrupt the normal operation of the Reputation Market, affecting trust and usability.
  • Integration Risks: If external systems or aggregators rely on successful execution of buyVotes, they may experience operational issues or unintended behavior.

While the vulnerability primarily affects individual transactions, its presence can undermine the reliability and robustness of the Reputation Market, warranting attention to prevent potential disruptions.


Vulnerability Details

The vulnerability stems from the implementation of an unbounded while-loop within the buyVotes function. This loop is intended to decrement the number of votes to purchase (currentVotesToBuy) until the cost is within the user's sent ETH (msg.value). However, without a mechanism to cap the number of iterations or ensure termination, an attacker can exploit this by setting maxVotesToBuy to an excessively high value, causing the loop to run indefinitely until the transaction exceeds the block gas limit and reverts.

Code Snippet

function buyVotes(
    uint256 profileId,
    bool isPositive,
    uint256 maxVotesToBuy,
    uint256 minVotesToBuy
) public payable whenNotPaused activeMarket(profileId) nonReentrant {
    _checkMarketExists(profileId);
    // preliminary check to ensure this is enough money to buy the minimum requested votes.
    (, , , uint256 total) = _calculateBuy(markets[profileId], isPositive, minVotesToBuy);
    if (total > msg.value) revert InsufficientFunds();

    (
        uint256 purchaseCostBeforeFees,
        uint256 protocolFee,
        uint256 donation,
        uint256 totalCostIncludingFees
    ) = _calculateBuy(markets[profileId], isPositive, maxVotesToBuy);
    uint256 currentVotesToBuy = maxVotesToBuy;
    // if the cost is greater than the maximum votes to buy,
    // decrement vote count and recalculate until we identify the max number of votes they can afford
    while (totalCostIncludingFees > msg.value) {
        currentVotesToBuy--;
        (purchaseCostBeforeFees, protocolFee, donation, totalCostIncludingFees) = _calculateBuy(
            markets[profileId],
            isPositive,
            currentVotesToBuy
        );
    }

    // ... rest of the function ...
}

Test Code Snippet

// Attempt to buy an excessively large number of votes with insufficient ETH
function testUnboundedLoop() public {
    // Arrange
    uint256 profileId = 42;
    bool isPositive = true;
    uint256 maxVotesToBuy = 9999999999; // Extremely large number
    uint256 minVotesToBuy = 1;
    uint256 msgValue = 0.5 ether; // Insufficient ETH

    // Act & Assert
    // Expect the transaction to revert due to gas exhaustion
    vm.expectRevert();
    reputationMarket.buyVotes{value: msgValue}(profileId, isPositive, maxVotesToBuy, minVotesToBuy);
}

Exploit Scenario:

  1. Initialization: A user invokes the buyVotes function with a maxVotesToBuy value set to a very high number (e.g., 9,999,999,999) and sends insufficient ETH (msg.value).
  2. Loop Execution: The function enters a while-loop, decrementing currentVotesToBuy by one in each iteration and recalculating the total cost.
  3. Gas Consumption: Due to the high initial maxVotesToBuy, the loop may execute millions or billions of times, consuming significant gas.
  4. Transaction Reversion: The transaction ultimately fails when the gas limit is exceeded, causing the entire call to revert.

This behavior effectively DoS-es the function for the user attempting the exploit and can indirectly affect other system components reliant on successful executions of buyVotes.


Tools Used

  • Manual Review
  • Foundry

Recommendations

Adopt a Binary Search Approach:

  • Replace the linear decrement with a binary search algorithm to determine the maximum affordable currentVotesToBuy in logarithmic time.
  • This significantly reduces gas consumption and ensures the function terminates efficiently.