Nice Pearl Canary
Medium
The Permanent Market Freeze Through Liquidity Exhaustion vulnerability in the ReputationMarket.sol
contract allows malicious actors to deplete the market’s liquidity, rendering the market permanently inactive. This occurs through the exploitation of the sellVotes()
function, which inadequately manages the reduction of marketFunds
when users sell their votes.
Impact:
- Market Inaccessibility: Once liquidity is drained below a critical threshold, the market becomes unusable for all participants. Remaining vote holders are unable to exit their positions, effectively locking their assets.
- Operational Disruption: The inability to perform trades compromises the core functionality of the Reputation Market, undermining user trust and the protocol’s integrity.
- Dependency on Graduation: Resolving the freeze requires market graduation, which may not be promptly actionable, prolonging the market’s inaccessibility.
While the issue does not directly result in financial loss or unauthorized access, it disrupts the intended operations and user interactions within the protocol, justifying a medium severity classification.
The vulnerability arises from the sellVotes()
function in the ReputationMarket.sol
contract. Specifically, the function allows users to sell their votes without enforcing a safeguard to prevent the depletion of marketFunds
below an operational threshold. This oversight enables attackers to drain the market’s liquidity, leading to a permanent freeze of the market.
function sellVotes(
uint256 profileId,
bool isPositive,
uint256 votesToSell,
uint256 minimumVotePrice
) public whenNotPaused activeMarket(profileId) nonReentrant {
_checkMarketExists(profileId);
(uint256 proceedsBeforeFees, uint256 protocolFee, uint256 proceedsAfterFees) = _calculateSell(
markets[profileId],
profileId,
isPositive,
votesToSell
);
uint256 pricePerVote = votesToSell > 0 ? proceedsBeforeFees / votesToSell : 0;
if (pricePerVote < minimumVotePrice) {
revert SellSlippageLimitExceeded(minimumVotePrice, pricePerVote);
}
markets[profileId].votes[isPositive ? TRUST : DISTRUST] -= votesToSell;
votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesToSell;
// Tally market funds
marketFunds[profileId] -= proceedsBeforeFees;
// Apply protocol fees
applyFees(protocolFee, 0, profileId);
// Send the proceeds to the seller
_sendEth(proceedsAfterFees);
emit VotesSold(
profileId,
msg.sender,
isPositive,
votesToSell,
proceedsAfterFees,
block.timestamp
);
_emitMarketUpdate(profileId);
}
-
Market Creation and Initial Setup:
- An attacker (User2) acquires a significant number of votes by purchasing them through
buyVotes()
. - The attacker ensures that the market is active and holds enough votes to influence liquidity.
- An attacker (User2) acquires a significant number of votes by purchasing them through
-
Liquidity Drainage:
- The attacker initiates multiple
sellVotes()
transactions to withdraw ETH from the market. - Each sell operation reduces
marketFunds[profileId]
byproceedsBeforeFees
without verifying whether the remaining funds are sufficient for future transactions.
- The attacker initiates multiple
-
Market Freeze:
- After draining a substantial portion of
marketFunds
, the attacker performs a finalsellVotes()
operation that reduces liquidity below the operational threshold. - Subsequent users attempting to sell their votes encounter insufficient funds, effectively freezing the market.
- After draining a substantial portion of
The contract’s invariant stipulates that the contract must never pay out the initial liquidity deposited as part of trading. However, the sellVotes()
function allows marketFunds
to be reduced without enforcing a lower bound, thereby violating this invariant. This oversight enables the market's liquidity to be exhausted, contradicting the system's expected behavior of maintaining operational funds until market graduation.
To mitigate the Permanent Market Freeze Through Liquidity Exhaustion vulnerability, the following measures are recommended:
- Implement a Minimum Liquidity Reserve:
- Introduce a minimum threshold for
marketFunds
that must remain untouched to ensure ongoing market operations. - Modify the
sellVotes()
function to include a check that preventsmarketFunds[profileId]
from dropping below this reserve.
- Introduce a minimum threshold for