Skip to content

Latest commit

 

History

History
127 lines (82 loc) · 3.97 KB

File metadata and controls

127 lines (82 loc) · 3.97 KB

Festive Mint Platypus

Medium

LMSR Invariant can break for getVotePrice

Summary

The invariant described in the code states that the price of trust vote and the distrust vote must always sum up to the market's base price. However there are cases when the getVotePrice(trust) + getVotePrice(distrust) < base price

Root Cause

In ReputationMarket.sol:1000 the developer makes the assumption to round up one of the values and round down the other value so that the sum total will preserved (and be equal to base price)

While this may be true in some cases

v1 =  floor(a / (a + b))
v2 =  ceil(b / (a + b))
v1 + v2 == 1 

it doesn't necessarily hold true due to solidity's precision errors when multiplying integers

There are multiple cases that can be generated which will break this rule all under the constraints laid out by the developer as shown in the fuzzing suite below

Internal Pre-conditions

N/A

External Pre-conditions

N/A

Attack Path

N/A

Impact

When users call the getVotePrice() function to make a decision to purchase, this sum value deflatation may confuse them.

This would be a low finding since it's in the view function and the buyVotes or the sellVotes don't necessarily use the same getVotePrice function to calculate the changes.

However this condition is stated as an invariant hence I am marking it as Medium.

PoC

Add the following in rep.market.test.ts

  describe('POC: LMSR invariant check fails for view function', () => {
    it('sums up the voting prices to a value less than base price', async () => {

      await userA.buyVotes({ votesToBuy: 109090n, isPositive: true });
      await userA.buyVotes({ votesToBuy: 3500n, isPositive: false });

      const market = await reputationMarket.getMarket(DEFAULT.profileId);
      expect(market.trustVotes).to.be.equal(109091n);
      expect(market.distrustVotes).to.be.equal(3501n);

      const trustVotePrice = await reputationMarket.getVotePrice(DEFAULT.profileId, true);
      const distrustVotePrice = await reputationMarket.getVotePrice(DEFAULT.profileId, false);

      expect(trustVotePrice + distrustVotePrice).to.be.lessThan(market.basePrice);

    });
  });

If you want to generate more such values for a diverse range of properties checkout the below fuzz test

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {LMSR} from "../src/LMSR.sol";
import {console2 as console} from "forge-std/console2.sol";
import "forge-std/Test.sol";

contract Confirm is Test {
    using Math for uint256;
    uint256 public constant DEFAULT_PRICE = 0.01 ether;
    uint256 public constant MINIMUM_BASE_PRICE = 0.0001 ether;

    // Use this fuzz test to generate examples
    function testFuzz_OddsSumTo1(
        uint256 basePrice,
        uint256 trust,
        uint256 distrust,
        uint256 liquidityParameter
    ) public pure {
        liquidityParameter = bound(liquidityParameter, 10_000, 100_000);
        trust = bound(trust, 1, 133 * liquidityParameter);
        distrust = bound(distrust, 1, 133 * liquidityParameter);
        basePrice = bound(basePrice, MINIMUM_BASE_PRICE, DEFAULT_PRICE);

        uint256 ratio1 = LMSR.getOdds(
            trust,
            distrust,
            liquidityParameter,
            true
        );
        uint256 cost1 = ratio1.mulDiv(basePrice, 1e18, Math.Rounding.Floor);

        uint256 ratio2 = LMSR.getOdds(
            trust,
            distrust,
            liquidityParameter,
            false
        );
        uint256 cost2 = ratio2.mulDiv(basePrice, 1e18, Math.Rounding.Ceil);

        assertEq(cost1 + cost2, basePrice);
    } 

Mitigation