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 - Initial Liquidity Could Be Paided. #140

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

Comments

@sherlock-admin3
Copy link
Contributor

Magnificent Tortilla Eel

High

Initial Liquidity Could Be Paided.

Summary

There is an unsafe rounding mode in ReputationMarket::L1057.

Root Cause

https://github.com/sherlock-audit/2024-12-ethos-update/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L1057
In the _calcCost() function, the cost is rounded down for TRUST and rounded up for DISTRUST.
If there are more times purchases than sales for TRUST, or fewer times purchases than sales for DISTRUST, the marketFunds can be consumed.
This occurs because there are more additions when rounding down and more subtractions when rounding up.

Internal pre-conditions

  • markets.liquidityParameter * markets.basePrice % 1e18 != 0

If markets.liquidityParameter * markets.basePrice % 1e18 != 0, the cost can be a non-integer value.
At this point, the rounding mode is used.

External pre-conditions

N/A

Attack Path

  1. Buy TRUST voting rights one at a time n times, then sell all these voting rights at once.
  2. Alternatively, buy n DISTRUST voting rights at once and then sell each vote one at a time for n times.

Impact

In Details:

What properties/invariants do you want to hold even if breaking them has a low/unknown impact?
The contract must never pay out the initial liquidity deposited as part of trading. The only way to access those funds is to graduate the market.

However, the initial liquidity could be Paided.

PoC

1017:   function _calcCost(
            Market memory market,
            bool isPositive,
            bool isBuy,
            uint256 amount
        ) private pure returns (uint256 cost) {
            // cost ratio is a unitless ratio of N / 1e18
            uint256[] memory voteDelta = new uint256[](2);
            // convert boolean input into market state change
            if (isBuy) {
                if (isPositive) {
                    voteDelta[0] = market.votes[TRUST] + amount;
                    voteDelta[1] = market.votes[DISTRUST];
                } else {
                    voteDelta[0] = market.votes[TRUST];
                    voteDelta[1] = market.votes[DISTRUST] + amount;
                }
            } else {
                if (isPositive) {
                    voteDelta[0] = market.votes[TRUST] - amount;
                    voteDelta[1] = market.votes[DISTRUST];
                } else {
                    voteDelta[0] = market.votes[TRUST];
                    voteDelta[1] = market.votes[DISTRUST] - amount;
                }
            }

            int256 costRatio = LMSR.getCost(
                market.votes[TRUST],
                market.votes[DISTRUST],
1047:           voteDelta[0],
1048:           voteDelta[1],
                market.liquidityParameter
            );

            uint256 positiveCostRatio = costRatio > 0 ? uint256(costRatio) : uint256(costRatio * -1);
            // multiply cost ratio by base price to get cost; divide by 1e18 to apply ratio
            cost = positiveCostRatio.mulDiv(
                market.basePrice,
                1e18,
1057:           isPositive ? Math.Rounding.Floor : Math.Rounding.Ceil
            );
        }

Let's examine how the initial liquidity is Paided.
Assume: liquidity := 1000, basePrice := 0.01e18 + 3e14, votes[0] := votes[1] := 1 and marketFunds = 0.2e18.
Buy TRUST voting rights one at a time n times, then sell all these voting rights at once.
Also, buy n DISTRUST votes and sell each of these votes n times.
Regarding this n:
n := 2 : marketFunds = 199999999999999999
n := 3 : marketFunds = 199999999999999998
n := 4 : marketFunds = 199999999999999997
n := 5 : marketFunds = 199999999999999996
n := 6 : marketFunds = 199999999999999996
n := 7 : marketFunds = 199999999999999995
n := 8 : marketFunds = 199999999999999994
n := 9 : marketFunds = 199999999999999992

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); marketFunds = Decimal(0.2e18); 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 _cost(yesVotes, noVotes, liquidityParameter):
    (yesExp, noExp) = _getExponentials(yesVotes, noVotes, liquidityParameter);
    sumExp = yesExp + noExp;
    lnVal = (uUNIT * (sumExp / uUNIT).ln()).to_integral_value(rounding = ROUND_FLOOR);
    costResult = lnVal * liquidityParameter;
    return costResult;
def getCost(currentYesVotes, currentNoVotes, outcomeYesVotes, outcomeNoVotes, liquidityParameter):
    oldCost = _cost(currentYesVotes, currentNoVotes, liquidityParameter);
    newCost = _cost(outcomeYesVotes, outcomeNoVotes, liquidityParameter);
    costDiff = newCost - oldCost;
    return costDiff;
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 _calcCost(isPositive, isBuy, amount):
    voteDelta = [0] * 2;
    #voteDelta[0] = votes[1]; voteDelta[1] = votes[0];
    #voteDelta[1-isPositive] += amount if isBuy else -amount;
    if (isBuy) :
        if (isPositive) :
            voteDelta[0] = votes[TRUST] + amount;
            voteDelta[1] = votes[DISTRUST];
        else :
            voteDelta[0] = votes[TRUST];
            voteDelta[1] = votes[DISTRUST] + amount;
    else :
        if (isPositive) :
            voteDelta[0] = votes[TRUST] - amount;
            voteDelta[1] = votes[DISTRUST];
        else :
            voteDelta[0] = votes[TRUST];
            voteDelta[1] = votes[DISTRUST] - amount;
    costRatio = getCost(votes[TRUST], votes[DISTRUST], voteDelta[0], voteDelta[1], liquidity);
    positiveCostRatio =  (costRatio) if costRatio > 0 else -costRatio;
    cost = mulDiv(positiveCostRatio, basePrice, uUNIT, Floor if isPositive else Ceil);
    return cost;
def _calculateBuy(isPositive, votesToBuy):
    purchaseCostBeforeFees = _calcCost(isPositive, 1, votesToBuy);
    return purchaseCostBeforeFees;
def buyVotes(isPositive,VotesToBuy):
    global marketFunds, votes;
    purchaseCostBeforeFees = _calculateBuy(isPositive,VotesToBuy);
    votes[1 if isPositive else 0] += VotesToBuy;
    marketFunds += purchaseCostBeforeFees;
def _calculateSell(isPositive, votesToSell):
    proceedsBeforeFees = _calcCost(isPositive, 0, votesToSell);
    return proceedsBeforeFees;
def sellVotes(isPositive,votesToSell):
    global marketFunds, votes;
    proceedsBeforeFees = _calculateSell(isPositive, votesToSell);
    votes[1 if isPositive else 0] -= votesToSell;
    marketFunds -= proceedsBeforeFees;

for n in range(2,10,1):
    votes = [Decimal(1)] * 2;
    marketFunds = Decimal(0.2e18);
    for _ in range(n):
        buyVotes(TRUST, Decimal(1));
    sellVotes(TRUST, Decimal(n));
    buyVotes(DISTRUST, Decimal(n));
    for _ in range(n):
        sellVotes(DISTRUST, Decimal(1));
    #if marketFunds < Decimal(0.2e18):
    print(f"`n := {n} : marketFunds = {marketFunds}`");

Mitigation

1017:   function _calcCost(
            Market memory market,
            bool isPositive,
            bool isBuy,
            uint256 amount
        ) private pure returns (uint256 cost) {
            // cost ratio is a unitless ratio of N / 1e18
            uint256[] memory voteDelta = new uint256[](2);
            // convert boolean input into market state change
            if (isBuy) {
            if (isPositive) {
                voteDelta[0] = market.votes[TRUST] + amount;
                voteDelta[1] = market.votes[DISTRUST];
            } else {
                voteDelta[0] = market.votes[TRUST];
                voteDelta[1] = market.votes[DISTRUST] + amount;
            }
            } else {
                if (isPositive) {
                    voteDelta[0] = market.votes[TRUST] - amount;
                    voteDelta[1] = market.votes[DISTRUST];
                } else {
                    voteDelta[0] = market.votes[TRUST];
                    voteDelta[1] = market.votes[DISTRUST] - amount;
                }
            }

            int256 costRatio = LMSR.getCost(
                market.votes[TRUST],
                market.votes[DISTRUST],
                voteDelta[0],
                voteDelta[1],
                market.liquidityParameter
            );

            uint256 positiveCostRatio = costRatio > 0 ? uint256(costRatio) : uint256(costRatio * -1);
            // multiply cost ratio by base price to get cost; divide by 1e18 to apply ratio
            cost = positiveCostRatio.mulDiv(
                market.basePrice,
                1e18,
-1057:          isPositive ? Math.Rounding.Floor : Math.Rounding.Ceil
+1057:          isBuy ? Math.Rounding.Ceil : Math.Rounding.Floor
            );
        }
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