Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Magnificent Tortilla Eel - TRUST + DISTRUST Price May Not Equal One #141

Open
sherlock-admin2 opened this issue Dec 30, 2024 · 0 comments
Open

Comments

@sherlock-admin2
Copy link

Magnificent Tortilla Eel

Medium

TRUST + DISTRUST Price May Not Equal One

Summary

The condition getOdds(isYes) + getOdds(isNo) < 1 may occur, leading to the TRUST price + DISTRUST price being less than one.

Root Cause

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

Internal pre-conditions

N/A

External pre-conditions

N/A

Attack Path

N/A

Impact

In Details:

Are there any limitations on values set by admins (or other roles) in protocols you integrate with, including restrictions on array lengths?

  • Must maintain LMSR invariant (yes + no price sum to 1)

In ReputationMarket.sol:

  • ... with both prices always summing to the market's basePrice.

However, the sum may less than basePrice.

PoC

ReputationMarket.sol
    function _calcVotePrice(Market memory market, bool isPositive) private pure returns (uint256) {
        // odds are in a ratio of N / 1e18
        uint256 odds = LMSR.getOdds(
            market.votes[TRUST],
            market.votes[DISTRUST],
            market.liquidityParameter,
            isPositive
        );
        // multiply odds by base price to get price; divide by 1e18 to get price in wei
        // round up for trust, down for distrust so that prices always equal basePrice
        return
1003:       odds.mulDiv(market.basePrice, 1e18, isPositive ? Math.Rounding.Floor : Math.Rounding.Ceil);
    }
LMSR.sol
    function getOdds(
        uint256 yesVotes,
        uint256 noVotes,
        uint256 liquidityParameter,
        bool isYes
    ) public pure returns (uint256 ratio) {
        // Compute exponentials e^(yes/b) and e^(no/b)
        (UD60x18 yesExp, UD60x18 noExp) = _getExponentials(yesVotes, noVotes, liquidityParameter);

        // sumExp = e^(yes/b) + e^(no/b)
        UD60x18 sumExp = yesExp.add(noExp);

        // priceRatio = e^(yes/b)/(sumExp) if isYes, else e^(no/b)/(sumExp)
70:     UD60x18 priceRatio = isYes ? yesExp.div(sumExp) : noExp.div(sumExp);

        // Unwrap to get  scaled ratio
        ratio = unwrap(priceRatio);
    }

Because both getOdds(,,,1) and getOdds(,,,0) are rounded down, getOdds(,,,1) + getOdds(,,,0) may less than 1e18.
calcVotePrice(,1) = floor(getOdds(,,,1) * basePrice / 1e18)
calcVotePrice(,0) = ceil(getOdds(,,,0) * basePrice / 1e18)
Consequently, calcVotePrice(,1) + calcVotePrice(,0) may less than market.basePrice.
At least, if getOdds(,,,0) * basePrice % 1e18 := 0:
calcVotePrice(,1) + calcVotePrice(,0) =
= floor(getOdds(,,,1) * basePrice / 1e18) + (getOdds(,,,0) * basePrice / 1e18) <=
<= ( getOdds(,,,1) + getOdds(,,,0) ) * basePrice / 1e18 <=
<= (1e18-1) * basePrice / 1e18 < basePrice.
Let's examine how the sum is less than basePrice.
Assume: liquidity := 1000, basePrice := 0.01e18 + 3e14, and marketFunds = 0.2e18.
Regarding votes[0] and votes[1]:
votes[0] := 61 votes[1] := 500 sumPrice = 10299999999999999
votes[0] := 102 votes[1] := 500 sumPrice = 10299999999999999
votes[0] := 349 votes[1] := 500 sumPrice = 10299999999999999
votes[0] := 395 votes[1] := 500 sumPrice = 10299999999999999
votes[0] := 461 votes[1] := 500 sumPrice = 10299999999999999
votes[0] := 539 votes[1] := 500 sumPrice = 10299999999999999
votes[0] := 621 votes[1] := 500 sumPrice = 10299999999999999
votes[0] := 651 votes[1] := 500 sumPrice = 10299999999999999
votes[0] := 898 votes[1] := 500 sumPrice = 10299999999999999
votes[0] := 939 votes[1] := 500 sumPrice = 10299999999999999
...
In these cases, sumPrice < basePrice.

Here is the Python code used for testing:

from decimal import Decimal, getcontext, ROUND_FLOOR
import math
getcontext().prec = 50; getcontext().rounding = ROUND_FLOOR;
TRUST = 1; DISTRUST = 0; Floor = 0; Ceil = 1; uUNIT = Decimal(1e18);
liquidity = Decimal(1000); basePrice = Decimal(0.01e18 + 3e14); votes = [Decimal(1)] * 2;

def div(x,y): # UD60x18
    result = (uUNIT * x / y).to_integral_value(rounding = ROUND_FLOOR);
    return result;
def _getExponentials(yesVotes, noVotes, liquidityParameter):
    yesUD = yesVotes * uUNIT;       # Convert to UD60x18
    noUD = noVotes * uUNIT;         # Convert to UD60x18
    b = liquidityParameter * uUNIT; # Convert to UD60x18
    yesRatio = div(yesUD , b);
    noRatio = div(noUD , b);
    yesExp = ((yesRatio / uUNIT).exp() * uUNIT).to_integral_value(rounding = ROUND_FLOOR);
    noExp = ((noRatio / uUNIT).exp() * uUNIT).to_integral_value(rounding = ROUND_FLOOR);
    return (yesExp, noExp);
def mulDiv(x,y,z,mode):
    res = (x * y / z).to_integral_value(rounding = ROUND_FLOOR);
    if (mode == Ceil):
        if (res * z != x * y):
            res += 1;
    return res;
def getOdds(yesVotes, noVotes, liquidityParameter, isYes):
    (yesExp, noExp) = _getExponentials(yesVotes, noVotes, liquidityParameter);
    sumExp = yesExp + noExp;
    priceRatio = div(yesExp,sumExp) if isYes else div(noExp,sumExp);
    return priceRatio;
def _calcVotePrice(isPositive):
    odds = getOdds(votes[1], votes[0], liquidity, isPositive);
    return mulDiv(odds, basePrice, uUNIT, Floor if isPositive else Ceil);
 
for d in range(-499,500,1):
    votes[0] = Decimal(500+d);
    votes[1] = Decimal(500);
    yesPrice = _calcVotePrice(TRUST);
    noPrice = _calcVotePrice(DISTRUST);
    if (yesPrice + noPrice < basePrice):
        print(f"`votes[0] := {500+d:3} votes[1] := {500:3} sumPrice = {yesPrice + noPrice}`");

Mitigation

LMSR.sol
    function getOdds(
        uint256 yesVotes,
        uint256 noVotes,
        uint256 liquidityParameter,
        bool isYes
    ) public pure returns (uint256 ratio) {
        // Compute exponentials e^(yes/b) and e^(no/b)
        (UD60x18 yesExp, UD60x18 noExp) = _getExponentials(yesVotes, noVotes, liquidityParameter);

        // sumExp = e^(yes/b) + e^(no/b)
        UD60x18 sumExp = yesExp.add(noExp);

        // priceRatio = e^(yes/b)/(sumExp) if isYes, else e^(no/b)/(sumExp)
-70:    UD60x18 priceRatio = isYes ? yesExp.div(sumExp) : noExp.div(sumExp);
+70:    UD60x18 priceRatio = isYes ? yesExp.div(sumExp) : 1e18 - yesExp.div(sumExp);

        // Unwrap to get  scaled ratio
        ratio = unwrap(priceRatio);
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant