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

Sparkly Ruby Rabbit - Zeroed-Out "Zombie" Markets #129

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

Sparkly Ruby Rabbit - Zeroed-Out "Zombie" Markets #129

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

Comments

@sherlock-admin2
Copy link

Sparkly Ruby Rabbit

Medium

Zeroed-Out "Zombie" Markets

Summary

A sequence of traders (including potentially malicious users) will lock a market into an inaccessible state for everyone by driving both trust and distrust votes to zero, leaving residual funds stuck.

The contract’s _checkMarketExists() function reverts if both markets[profileId].votes[TRUST] == 0 and markets[profileId].votes[DISTRUST] == 0. Once a market’s vote counts reach (0, 0), all further interactions (buy or sell) revert. This effectively becomes a “zombie” market: it can’t be used, even though marketFunds[profileId] might still hold ETH.

Impact
This causes a permanent lock for users, as no one can re-populate the market with new votes (the check reverts). Any leftover ETH remains trapped until/unless an authorized entity graduates or forcefully manages the market.

Root Cause

In ReputationMarket.sol, _checkMarketExists():

function _checkMarketExists(uint256 profileId) private view {
    if (markets[profileId].votes[TRUST] == 0 && markets[profileId].votes[DISTRUST] == 0)
        revert MarketDoesNotExist(profileId);
}

Any function calling _checkMarketExists(profileId) (e.g., buyVotes(), sellVotes(), simulateBuy(), etc.) will revert if the votes are (0,0). There is no re-creation or re-initialization path for a zeroed-out market.

Internal Pre-conditions

1-The market has small enough total votes that it’s possible for users to fully sell all trust and all distrust votes.
2-No logic or check prevents both sides from going to zero simultaneously.

External Pre-conditions

1-Traders systematically sell (or burn) all trust votes and all distrust votes.
2-The market is not graduated and still has marketFunds[profileId] > 0 left behind.

Attack Path

1-A user sees the market has (x, y) trust/distrust votes.
2-They and others sell until x and y become 0 and 0.
3- _checkMarketExists() triggers revert MarketDoesNotExist(...) for any new buyVotes() call. Thus, no one can “reopen” the market by buying again.
4- If the graduation function is never invoked, the leftover marketFunds[profileId] can become stuck indefinitely.

(Though not a direct “theft,” it’s a scenario that denies further usage or retrieval by normal participants.)

Impact

Affected Party:

  • All potential market participants hoping to keep trading or to reinitiate a zeroed market.
  • Potential leftover liquidity that remains locked if the official graduation contract never calls withdrawGraduatedMarketFunds().

Result:

  • Denial of Service for future trades.
  • Possible indefinite lock of any remaining ETH in marketFunds[profileId].

PoC

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import "forge-std/Test.sol";
import "../src/ReputationMarket.sol";

contract ZombieMarketTest is Test {
    ReputationMarket rm;
    uint256 profileId = 12345;

    function setUp() public {
        rm = new ReputationMarket();
        // Assume some initialization & market creation, so
        // markets[profileId].votes[TRUST] = 1, markets[profileId].votes[DISTRUST] = 1
        // marketFunds[profileId] = 1 ether
    }

    function testZeroedOutMarket() public {
        // Suppose user sells the last TRUST vote
        // now trust votes = 0
        // Another user sells the last DISTRUST vote
        // now distrust votes = 0 => combined = (0,0)

        // Any call that checks _checkMarketExists(profileId) reverts:
        vm.expectRevert(ReputationMarketErrors.MarketDoesNotExist.selector);
        rm.buyVotes{value: 0.1 ether}(profileId, true, 1, 1);

        // leftover funds remain in marketFunds[profileId], but no normal path to reclaim them 
        // unless the official "graduateMarket" => "withdrawGraduatedMarketFunds" is triggered 
        // by the authorized address.
    }
}

Mitigation

1-Automatically Graduate when (trustVotes, distrustVotes) == (0, 0).
Once both sides are zero, call graduateMarket(profileId) internally, allowing an official withdrawal flow.

2-Allow Re-Initialization
Provide an explicit function for an admin or the original market owner to “revive” a zeroed market by setting (1,1) again.

3-Prohibit Simultaneous Zeroing
Revert any sell that would make a side go zero if the other side is already zero (though this might be too restrictive for legitimate trades).

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