Glamorous Canvas Camel
Medium
The ReputationMarket
contract contains an error in the cost calculation within the sellVotes
function. Specifically, when calculating the proceeds from selling votes, the contract incorrectly handles negative cost differences by taking the absolute value, which can lead to users withdrawing more funds than they should receive. This miscalculation occurs because the _calcCost
function ignores the sign of the cost difference (costDiff
), leading to proceeds being incorrectly treated as positive even when they should be negative.
In the _calcCost
function, the cost difference (costRatio
) calculated by the LMSR.getCost
function can be negative when selling votes. However, _calcCost
takes the absolute value of costRatio
without considering whether it's a gain or a loss, resulting in incorrect cost calculations.
int256 costRatio = LMSR.getCost(
market.votes[TRUST],
market.votes[DISTRUST],
voteDelta[0],
voteDelta[1],
market.liquidityParameter
);
uint256 positiveCostRatio = costRatio > 0 ? uint256(costRatio) : uint256(-costRatio);
// Multiply cost ratio by base price to get cost
cost = positiveCostRatio.mulDiv(
market.basePrice,
1e18,
isPositive ? Math.Rounding.Floor : Math.Rounding.Ceil
);
In the sellVotes
function, the proceeds before fees (proceedsBeforeFees
) are calculated using _calcCost
, which, due to the absolute value handling, returns a positive value even when costRatio
is negative. This leads to users potentially receiving funds from the contract when they should actually be paying into the market.
proceedsBeforeFees = _calcCost(market, isPositive, false, votesToSell);
The incorrect calculation results in marketFunds[profileId]
being decreased by the (incorrectly positive) proceedsBeforeFees
, which can cause the contract's total balance to fall below the correct amount, violating financial invariants and potentially leading to loss of funds.
No response
No response
An attacker can exploit this flaw by selling votes in a scenario where the cost difference (costDiff
) is negative. Due to the absolute value handling in _calcCost
, the proceeds are incorrectly calculated as a positive amount, allowing the attacker to receive ETH from the contract when they actually should be paying into it.
Example Steps:
-
Market Manipulation:
- The attacker manipulates the market to create conditions where selling their votes results in a negative
costDiff
. This could involve purchasing a large number of opposing votes to shift the market state.
- The attacker manipulates the market to create conditions where selling their votes results in a negative
-
Initiate Exploit:
- The attacker calls
sellVotes
to sell their votes.
- The attacker calls
-
Incorrect Proceeds Calculation:
- The
_calcCost
function computescostRatio
as negative due to the market state but then converts it to a positive value using the absolute value, leading to an incorrect positiveproceedsBeforeFees
.
- The
-
Withdrawal of Excess Funds:
- The attacker receives ETH from the contract, withdrawing more funds than appropriate and potentially draining the contract's balance.
The attacker receives ETH from the contract when they should have been required to pay, improperly withdrawing funds.
Assume the following initial state of the market:
-
Market Parameters:
uint256 yesVotes = 2000; uint256 noVotes = 1000; uint256 liquidityParameter = 1000; uint256 basePrice = 1 ether;
-
Attacker's Holding:
uint256 attackerYesVotes = 1000; // Attacker owns 1000 "yes" votes
The attacker manipulates the market to create a condition where selling their "yes" votes results in a negative cost difference.
- Market Shift:
- The attacker (or colluding parties) buys additional "no" votes to significantly change the market dynamics.
- New Market State:
yesVotes = 2000; noVotes = 5000; // Increased from 1000 to 5000 // The attacker's holdings remain the same
The attacker proceeds to sell their "yes" votes by calling the sellVotes
function.
// Attacker calls sellVotes to sell 1000 "yes" votes
reputationMarket.sellVotes(
profileId, // The profile ID of the market
true, // isPositive = true (selling "yes" votes)
1000, // votesToSell
0 // minimumVotePrice (set to 0 for simplicity)
);
Within the sellVotes
function, the proceeds are calculated using the _calcCost
function, which improperly handles negative cost differences.
Step-by-Step Calculation:
-
Calculate Proceeds Before Fees:
proceedsBeforeFees = _calcCost(market, isPositive, false, votesToSell);
-
Inside
_calcCost
:-
Determine Vote Delta for Selling Votes:
// Since isPositive is true and isBuy is false, we are selling "yes" votes voteDelta[0] = market.votes[TRUST] - amount; // Updated "yes" votes voteDelta[1] = market.votes[DISTRUST]; // "No" votes remain the same
After selling:
voteDelta[0] = 2000 - 1000 = 1000; voteDelta[1] = 5000;
-
Calculate Cost Difference Using LMSR:
int256 costRatio = LMSR.getCost( market.votes[TRUST], // Current "yes" votes: 2000 market.votes[DISTRUST], // Current "no" votes: 5000 voteDelta[0], // Updated "yes" votes: 1000 voteDelta[1], // Updated "no" votes: 5000 market.liquidityParameter // Liquidity parameter: 1000 );
-
In LMSR.getCost Function: The cost difference (
costDiff
) is calculated as:costDiff = newCost - oldCost;
Because the market has shifted significantly,
newCost
is less thanoldCost
, resulting in a negativecostDiff
. -
Incorrect Absolute Value Handling:
uint256 positiveCostRatio = costRatio > 0 ? uint256(costRatio) : uint256(-costRatio);
Here, even though
costRatio
is negative, taking the absolute value makespositiveCostRatio
positive. -
Calculate Cost:
cost = positiveCostRatio.mulDiv( market.basePrice, // basePrice = 1 ether 1e18, isPositive ? Math.Rounding.Floor : Math.Rounding.Ceil );
This results in a positive
cost
, which is incorrect because the cost difference should be negative.
-
-
Back to
sellVotes
:-
Proceeding with Incorrect Proceeds:
proceedsBeforeFees = /* incorrect positive value */;
-
Calculate Fees: Fees are calculated based on the incorrectly positive
proceedsBeforeFees
. -
Update Market Funds and User's Vote Holdings:
markets[profileId].votes[TRUST] -= votesToSell; // Deduct sold votes votesOwned[msg.sender][profileId].votes[TRUST] -= votesToSell; marketFunds[profileId] -= proceedsBeforeFees; // Decrease market funds incorrectly
-
Transfer Funds to Attacker:
_sendEth(proceedsAfterFees); // Attacker receives ETH
-
As a result of the incorrect cost calculation:
- The attacker receives ETH from the contract when, in reality, they should have paid ETH into the market due to the negative cost difference.
- The contract's
marketFunds
decreases improperly, potentially leading to a deficit. - This allows the attacker to withdraw excess funds from the contract.
Modify the _calcCost
function to return an int256
representing the actual cost difference, preserving its sign, and adjust the calculation accordingly.
function _calcCost(
Market memory market,
bool isPositive,
bool isBuy,
uint256 amount
) private pure returns (int256 cost) {
// Calculate the vote delta
uint256[] memory voteDelta = new uint256[](2);
if (isBuy) {
// ... existing logic ...
} else {
// ... existing logic ...
}
// Get the cost difference from the LMSR
int256 costRatio = LMSR.getCost(
market.votes[TRUST],
market.votes[DISTRUST],
voteDelta[0],
voteDelta[1],
market.liquidityParameter
);
// Multiply cost ratio by base price to get cost
cost = int256(market.basePrice).mulDiv(
costRatio,
1e18
);
}
-
Adjust Calling Functions to Handle Negative Proceeds:
In the
sellVotes
function, check if theproceedsBeforeFees
is negative. If it is, handle the case appropriately, such as requiring the seller to pay into the market or preventing the sale.int256 proceedsBeforeFees = _calcCost(market, isPositive, false, votesToSell); if (proceedsBeforeFees <= 0) { // The seller should not receive funds; optionally, they may need to pay in revert InvalidSellOperation("Proceeds cannot be negative"); } uint256 proceeds = uint256(proceedsBeforeFees); // Proceed with fee deductions and ETH transfer