Cheesy Pebble Caribou
Medium
At certain values of "Yes" and "No" votes, the cost of a vote becomes less than the probability, which should never be the case.
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).
None
The number of "Yes" votes is significantly larger compared to the number of "No" votes.
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.
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.
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)
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.