Skip to content

Latest commit

 

History

History
188 lines (137 loc) · 5.94 KB

File metadata and controls

188 lines (137 loc) · 5.94 KB

Cheesy Pebble Caribou

Medium

Cost Does Not Always Exceed Current Odds

Summary

At certain values of "Yes" and "No" votes, the cost of a vote becomes less than the probability, which should never be the case.

Root Cause

In the LMSR library code, a significant imbalance between "Yes" and "No" votes causes incorrect cost calculations in certain cases.

With:

yesVotes = 25801 noVotes = 100 b = 1000 The variation of ln ( sumExp ) ln (sumExp) between the initial state (yesVotes) and the final state (yesVotes + 1) is too small.

The cost (Δln (sumExp) Δln(sumExp)) becomes less than the current probability ( yesOdds yesOdds).

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

Internal Pre-conditions

None

External Pre-conditions

The number of "Yes" votes is significantly larger compared to the number of "No" votes.

Attack Path

Number of "Yes" votes: 25801 Number of "No" votes: 991009 We obtain the following results:

YesOdd = 999999999993110344 Cost = 999999999993110000 Result: The cost should exceed the current odds, but we have Cost <= YesOdd.

Impact

When the calculated cost becomes less than the current probability ( yesOdds yesOdds):

Malicious participants may exploit this difference to perform arbitrage and gain risk-free profits. The market loses economic consistency as it no longer adheres to the fundamental LMSR model rules, where each state change must be proportional to its impact on probabilities.

PoC

Create a test class using Foundry and run the following command:

forge test --mt testHigherPriceForMajorityVotes --via-ir -vvvv
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import { Test, console } from "forge-std/Test.sol";
import { LMSR } from "../../contracts/utils/LMSR.sol";
import { TestLMSR } from "./TestLMSR.sol";

/**
 * @title LMSRTests
 * @dev Test contract for the LMSR library using Foundry's testing framework.
 */
contract LMSRTests is Test {
  TestLMSR private lmsr;

  // Constants for testing
  uint256 private constant LIQUIDITY_PARAMETER = 1000; // Liquidity parameter for stable price calculations
  uint256 private constant QUOTIENT = 1e18; // Scaling factor for fixed-point arithmetic

  // Setup function to initialize the LMSR library
  function setUp() public {
    // Deploy LMSR library
    lmsr = new TestLMSR();
  }

 /**
   * @notice Test the cost calculation between current and next price for buying votes.
   */
  function testCostCurrentAndNextPriceForBuying() public {
    uint256 yes = 25801;
    uint256 no = 100;

    uint256 yesOdds = lmsr.getOdds(yes, no, LIQUIDITY_PARAMETER, true);
    uint256 nextYesOdds = lmsr.getOdds(yes + 1, no, LIQUIDITY_PARAMETER, true);
    int256 yesCost = lmsr.getCost(yes, no, yes + 1, no, LIQUIDITY_PARAMETER);
    assertGt(yesCost, int256(yesOdds), "Cost should exceed current odds");
    assertLt(yesCost, int256(nextYesOdds), "Cost should be below next odds");
  }
}

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import { LMSR } from "../../contracts/utils/LMSR.sol";

/**
 * @title TestLMSR
 * @dev Wrapper contract to expose LMSR library functions for testing.
 */
contract TestLMSR {
  using LMSR for *;

  // Expose LMSR library functions

  function getOdds(
    uint256 yesVotes,
    uint256 noVotes,
    uint256 liquidityParameter,
    bool isYes
  ) external pure returns (uint256) {
    return LMSR.getOdds(yesVotes, noVotes, liquidityParameter, isYes);
  }

  function getCost(
    uint256 currentYesVotes,
    uint256 currentNoVotes,
    uint256 outcomeYesVotes,
    uint256 outcomeNoVotes,
    uint256 liquidityParameter
  ) external pure returns (int256) {
    return
      LMSR.getCost(
        currentYesVotes,
        currentNoVotes,
        outcomeYesVotes,
        outcomeNoVotes,
        liquidityParameter
      );
  }
}

Test Output

Traces:
  [131173] LMSRTests::setUp()
    ├─ [93747] → new TestLMSR@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
    │   └─ ← [Return] 468 bytes of code
    └─ ← [Stop] 

  [52901] LMSRTests::testCostCurrentAndNextPriceForBuying()
    ├─ [9754] TestLMSR::getOdds(25801 [2.58e4], 100, 1000, true) [staticcall]
    │   ├─ [6635] LMSR::getOdds(25801 [2.58e4], 100, 1000, true) [delegatecall]
    │   │   └─ ← [Return] 999999999993110344 [9.999e17]
    │   └─ ← [Return] 999999999993110344 [9.999e17]
    ├─ [6806] TestLMSR::getOdds(25802 [2.58e4], 100, 1000, true) [staticcall]
    │   ├─ [6187] LMSR::getOdds(25802 [2.58e4], 100, 1000, true) [delegatecall]
    │   │   └─ ← [Return] 999999999993117230 [9.999e17]
    │   └─ ← [Return] 999999999993117230 [9.999e17]
    ├─ [26453] TestLMSR::getCost(25801 [2.58e4], 100, 25802 [2.58e4], 100, 1000) [staticcall]
    │   ├─ [25817] LMSR::getCost(25801 [2.58e4], 100, 25802 [2.58e4], 100, 1000) [delegatecall]
    │   │   └─ ← [Return] 999999999993110000 [9.999e17]
    │   └─ ← [Return] 999999999993110000 [9.999e17]
    ├─ [0] VM::assertGt(999999999993110000 [9.999e17], 999999999993110344 [9.999e17], "Cost should exceed current odds") [staticcall]
    │   └─ ← [Revert] Cost should exceed current odds: 999999999993110000 <= 999999999993110344
    └─ ← [Revert] Cost should exceed current odds: 999999999993110000 <= 999999999993110344

Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 6.07ms (1.83ms CPU time)

Ran 1 test suite in 851.10ms (6.07ms CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests)

Failing tests:
Encountered 1 failing test in test/foundry/LMSRTests.t.sol:LMSRTests
[FAIL: Cost should exceed current odds: 999999999993110000 <= 999999999993110344] testCostCurrentAndNextPriceForBuying() (gas: 52901)

Mitigation

Modify the LMSR.sol code to include a minimum cost adjustment in the getCost function when newCost < oldCost. This ensures newCost is always greater than oldCost.