Skip to content

Latest commit

 

History

History
122 lines (95 loc) · 4.84 KB

File metadata and controls

122 lines (95 loc) · 4.84 KB

Cool Mango Bobcat

Medium

No Price Impact Protection in Reputation Market Vote Trading Enables Price Manipulation and MEV

Summary

The buyVotes function in ReputationMarket.sol lacks essential price impact protection mechanisms, exposing users to sandwich attacks and price manipulation. While the function implements a minimum vote quantity check through minVotesToBuy, it fails to protect users from executing trades at highly unfavorable prices.

The root cause lies in the vote acquisition logic where trades can execute at arbitrarily high prices as long as some minimum vote quantity is obtained. The vulnerability manifests from the interaction between the price calculation mechanism and the trade execution loop:

The loop continues reducing vote quantity until the trade fits within provided ETH, but critically fails to validate the resulting execution price.

This creates a direct path for value extraction where sophisticated traders can manipulate the price through large trades, causing user transactions to execute at severely inflated prices. The impact ripples through the entire market mechanism, undermining its ability to accurately reflect reputation scores and creating opportunities for malicious actors to profit from regular user activity.

Proof of Concept

The core vulnerability exists in the buyVotes function

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

function buyVotes(
    uint256 profileId,
    bool isPositive,
    uint256 maxVotesToBuy,
    uint256 minVotesToBuy
) public payable whenNotPaused activeMarket(profileId) nonReentrant {
    _checkMarketExists(profileId);
    // First price check
    (, , , uint256 total) = _calculateBuy(markets[profileId], isPositive, minVotesToBuy);
    if (total > msg.value) revert InsufficientFunds();

    // Second price calculation
    (
        uint256 purchaseCostBeforeFees,
        uint256 protocolFee,
        uint256 donation,
        uint256 totalCostIncludingFees
    ) = _calculateBuy(markets[profileId], isPositive, maxVotesToBuy);
    
    uint256 currentVotesToBuy = maxVotesToBuy;
    // Vulnerable while loop - no price checks
    while (totalCostIncludingFees > msg.value) {
        currentVotesToBuy--;
        (purchaseCostBeforeFees, protocolFee, donation, totalCostIncludingFees) = _calculateBuy(
            markets[profileId],
            isPositive,
            currentVotesToBuy
        );
    }

    // Update market state without price validation
    markets[profileId].votes[isPositive ? TRUST : DISTRUST] += currentVotesToBuy;
    votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] += currentVotesToBuy;

    // Market funds updated after price could be manipulated
    marketFunds[profileId] += purchaseCostBeforeFees;
    
    applyFees(protocolFee, donation, profileId);

    uint256 refund = msg.value - totalCostIncludingFees;
    if (refund > 0) _sendEth(refund);
}

Scenario

  1. Initial state:
  • Vote price is 0.1 ETH
  • Target user submits transaction to buy 100 votes with 11 ETH budget
  • User expects to pay ~0.11 ETH per vote including fees
  1. Attacker execution:
  • Front-runs by buying 1000 votes, driving price up to 0.5 ETH per vote
  • User's transaction executes:
    • Cannot afford 100 votes at new price
    • Loop reduces order to only 20 votes
    • Pays 10 ETH for 20 votes (0.5 ETH per vote)
  • Back-runs by immediately selling 1000 votes
  • Price returns to ~0.1 ETH per vote
  1. Result:
  • User paid 5x expected price (0.5 ETH vs 0.1 ETH per vote)
  • Got 80% fewer votes than intended (20 vs 100)
  • Attacker profits from price spread between manipulated high and normal price
  • No slippage protection prevented the unfavorable execution

Recommended Mitigation Steps

The vulnerability can be addressed through a comprehensive approach to price protection:

The first layer of defense requires implementing maximum price impact validation in the trade execution:

function buyVotes(
    uint256 profileId,
    bool isPositive,
    uint256 maxVotesToBuy,
    uint256 minVotesToBuy,
    uint256 maxPricePerVote,
    uint256 deadline
) external payable {
    require(block.timestamp <= deadline, "Transaction expired");
    
    uint256 pricePerVote = totalCostIncludingFees / currentVotesToBuy;
    require(pricePerVote <= maxPricePerVote, "Price impact too high");
}

This should be complemented with price deviation checks based on time-weighted average prices:

function _checkPriceDeviation(
    uint256 currentPrice,
    uint256 twapPrice,
    uint256 maxDeviation
) internal pure returns (bool) {
    return (currentPrice <= twapPrice * (100 + maxDeviation) / 100);
}

The final layer should implement circuit breakers for large market moves and consider alternative price discovery mechanisms like batch auctions to provide structural protection against MEV.