Nice Pearl Canary
Medium
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.
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.
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 ...
}
// 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:
- Initialization: A user invokes the
buyVotes
function with amaxVotesToBuy
value set to a very high number (e.g., 9,999,999,999) and sends insufficient ETH (msg.value
). - Loop Execution: The function enters a while-loop, decrementing
currentVotesToBuy
by one in each iteration and recalculating the total cost. - Gas Consumption: Due to the high initial
maxVotesToBuy
, the loop may execute millions or billions of times, consuming significant gas. - 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
.
- Manual Review
- Foundry
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.