You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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(uint256profileId) privateview {
if (markets[profileId].votes[TRUST] ==0&& markets[profileId].votes[DISTRUST] ==0)
revertMarketDoesNotExist(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: MITpragma solidity^0.8.26;
import"forge-std/Test.sol";
import"../src/ReputationMarket.sol";
contractZombieMarketTestisTest {
ReputationMarket rm;
uint256 profileId =12345;
function setUp() public {
rm =newReputationMarket();
// 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.1ether}(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).
The text was updated successfully, but these errors were encountered:
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():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 newbuyVotes()
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:
Result:
marketFunds[profileId]
.PoC
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).
The text was updated successfully, but these errors were encountered: