Glamorous Canvas Camel
Medium
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.
The LMSR library defines a maximum safe number of votes per side to prevent overflows in exponential calculations:
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;
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.
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 of133,000
.
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.
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.
With trustVotes
now at 133,001
, any subsequent LMSR calculations involving trustVotes
will revert due to overflow checks in the LMSR library.
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)
}