Skip to content

Latest commit

 

History

History
209 lines (135 loc) · 7.25 KB

File metadata and controls

209 lines (135 loc) · 7.25 KB

Glamorous Canvas Camel

Medium

Votes Exceeding Safe Limits Allow Attackers to Cause Denial-of-Service on Markets

Summary

The ReputationMarket contract uses the LMSR (Logarithmic Market Scoring Rule) library for calculating odds and costs in the market. The LMSR library has a mathematical limitation: the number of votes per side (trust or distrust) must not exceed approximately 133 * liquidityParameter. Exceeding this limit causes LMSR calculations to revert due to overflow in the exponential function. The contract currently does not enforce this limit during vote purchases or sales, allowing an attacker to intentionally exceed the safe vote limit. By doing so, the attacker can cause all subsequent interactions with the market to fail, effectively creating a Denial-of-Service (DoS) condition that prevents legitimate users from buying, selling, or interacting with the affected market.

Root Cause

The LMSR library defines a maximum safe number of votes per side to prevent overflows in exponential calculations:

https://github.com/sherlock-audit/2024-12-ethos-update/blob/c3a2b007d0ddfcb476f300f8b766808f0e3e2dfd/ethos/packages/contracts/contracts/utils/LMSR.sol#L154C4-L164C6

   uint256 maxSafeRatio = 133;
   if (
       yesVotes > maxSafeRatio * liquidityParameter || noVotes > maxSafeRatio * liquidityParameter
   ) {
       revert VotesExceedSafeLimit(
           yesVotes > noVotes ? yesVotes : noVotes,
           liquidityParameter,
           maxSafeRatio * liquidityParameter
       );
   }
   ```

 - This limit is in place because the `exp()` function in the LMSR calculations can overflow if the input value (`votes / liquidityParameter`) exceeds approximately `133`.

- **No Checks in `ReputationMarket` for Vote Limits:**

 - The `ReputationMarket` contract does not enforce the maximum safe vote limits when users buy or sell votes.
 
 - Functions like `buyVotes` and `sellVotes` update the vote counts without checking against the LMSR safe limits:

   ```solidity
   // Example from buyVotes function
   markets[profileId].votes[isPositive ? TRUST : DISTRUST] += currentVotesToBuy;
   ```

- **Potential for Denial-of-Service:**

 - If the total votes for either trust or distrust exceed the safe limit, any function that relies on LMSR computations (e.g., `getVotePrice`, `buyVotes`, `sellVotes`) will revert due to overflow errors.

### Internal Pre-conditions

_No response_

### External Pre-conditions

_No response_

### Attack Path


An attacker can intentionally exceed the maximum safe votes for either trust or distrust votes in a market, causing all LMSR calculations to revert due to overflows. This prevents any further interactions with the market, denying service to all users.

### **Steps of the Attack**

1. **Identify Target Market:**

  - The attacker selects a specific market to target.

  - Retrieves the market's `liquidityParameter` to calculate the maximum safe vote limit.

2. **Calculate Maximum Safe Votes:**

  - Calculates `maxSafeVotes = 133 * liquidityParameter`.

  - Determines how many votes they need to purchase to exceed this limit.

3. **Accumulate Votes to Exceed Safe Limit:**

  - The attacker buys enough votes on one side (trust or distrust) to push the total votes over the safe limit.

  - There's no check in the `buyVotes` function to prevent this.

4. **Cause Denial-of-Service:**

  - Once the safe limit is exceeded, any LMSR function that involves the affected vote count will revert due to overflow checks in the LMSR library.

  - Functions like `buyVotes`, `sellVotes`, `getVotePrice`, and any other that relies on LMSR calculations will fail.


### Impact

Legitimate users will be unable to interact with the market, effectively causing a DoS condition on that market.

### PoC


From the LMSR code:

```solidity
uint256 maxSafeRatio = 133;
if (
   yesVotes > maxSafeRatio * liquidityParameter || noVotes > maxSafeRatio * liquidityParameter
) {
   revert VotesExceedSafeLimit(
       yesVotes > noVotes ? yesVotes : noVotes,
       liquidityParameter,
       maxSafeRatio * liquidityParameter
   );
}

Therefore, the maximum safe number of votes per side (trustVotes or distrustVotes) is calculated as:

maxSafeVotesPerSide = maxSafeRatio * liquidityParameter;

Calculations

Assuming the following market parameters:

  • Liquidity Parameter (b):

    uint256 liquidityParameter = 1_000; // Example value for liquidity parameter
  • Calculating Maximum Safe Votes Per Side:

    uint256 maxSafeVotesPerSide = 133 * liquidityParameter; // For b = 1,000, maxSafeVotesPerSide = 133,000 votes

This means that if either trustVotes or distrustVotes exceed 133,000 votes, the LMSR functions will revert due to safety checks to prevent overflow.

Attacker's Actions

1. Initial Market State

Let's assume the market currently has the following vote counts:

markets[profileId].votes[TRUST] = 100,000;     // Current trust votes
markets[profileId].votes[DISTRUST] = 50,000;   // Current distrust votes
  • The current trustVotes are below the maximum safe limit of 133,000.

2. Calculating Votes Needed to Exceed Safe Limit

The attacker wants to exceed the safe limit for trustVotes. They calculate the number of votes needed:

votesToBuy = (maxSafeVotesPerSide + 1) - currentTrustVotes;
// votesToBuy = (133,000 + 1) - 100,000 = 33,001 votes

By purchasing 33,001 additional trust votes, the attacker will push trustVotes to 133,001, exceeding the safe limit by 1 vote.

3. Executing the Attack

The attacker proceeds to purchase the calculated number of votes:

// Attacker calls buyVotes to purchase 33,001 trust votes
reputationMarket.buyVotes{value: totalCost}(
    profileId,
    true,         // isPositive = true (buying trust votes)
    33_001,       // votesToBuy
    minVotesToBuy // Minimum acceptable votes (for slippage protection)
);

After this transaction, the updated vote counts are:

markets[profileId].votes[TRUST] = 100,000 + 33,001 = 133,001 trust votes
markets[profileId].votes[DISTRUST] = 50,000;   // Unchanged distrust votes

This pushes the trustVotes over the maximum safe limit.

4. Resulting Denial-of-Service

With trustVotes now at 133,001, any subsequent LMSR calculations involving trustVotes will revert due to overflow checks in the LMSR library.

Mitigation

Before allowing a vote purchase, check whether the new total votes would exceed the maximum safe votes.

function buyVotes(
    uint256 profileId,
    bool isPositive,
    uint256 maxVotesToBuy,
    uint256 minVotesToBuy
) public payable whenNotPaused activeMarket(profileId) nonReentrant {
    _checkMarketExists(profileId);

    uint256 liquidityParameter = markets[profileId].liquidityParameter;
    uint256 maxSafeVotes = 133 * liquidityParameter;
    uint256 currentVotes = markets[profileId].votes[isPositive ? TRUST : DISTRUST];
    uint256 newTotalVotes = currentVotes + maxVotesToBuy;

    // Ensure purchase does not exceed safe vote limit
    if (newTotalVotes > maxSafeVotes) {
        revert("Purchase exceeds maximum safe votes for this market");
    }

    // ... (rest of the function)
}