Skip to content

Latest commit

 

History

History
119 lines (88 loc) · 4.66 KB

File metadata and controls

119 lines (88 loc) · 4.66 KB

Brilliant Myrtle Hamster

Medium

Missing Participant Status Reset in SellVotes Function Leads to Participant State Inconsistency

Summary

The sellVotes function in ReputationMarket contract fails to reset participant status when users sell all their votes. Participants are marked as active in isParticipant mapping when they first buy votes, but this status is never cleared even if they sell their entire position.

Root Cause

In ReputationMarket.sol sellVotes() function, there is no check or reset of the participant's status in isParticipant mapping even when users sell all their votes. The participant remains marked as active in isParticipant[profileId][msg.sender] even after selling their entire position, causing state inconsistency.

Code Snippet: https://github.com/sherlock-audit/2024-12-ethos-update/blob/c3a2b007d0ddfcb476f300f8b766808f0e3e2dfd/ethos/packages/contracts/contracts/ReputationMarket.sol#L539-#L578

Internal Pre-conditions

  1. User needs to have bought votes in a market to be registered as a participant
  2. User needs to be marked as a participant in isParticipant mapping
  3. User needs to have votes to sell in votesOwned mapping

External Pre-conditions

-- N/A --

Attack Path

  1. Attacker buys any amount of votes in a market using buyVotes()
  2. Attacker gets registered as participant in participants array and isParticipant mapping
  3. Attacker sells all their votes using sellVotes()
  4. Despite having no votes, attacker remains marked as an active participant
  5. getParticipantCount() continues to include the attacker in the total count
  6. The protocol considers the attacker an active participant for any participant-based logic

Impact

The protocol suffers from inaccurate participant accounting. This affects:

  1. Market statistics showing incorrect number of active participants
  2. Frontend displays showing misleading participant numbers to users

Creates inconsistent state and potentially affects protocol future governance/mechanics based on participant counts.

PoC

  1. Create a new folder and file in test/foundry/BugTest.sol and add the following gist code.
  1. To run the test, run the following command: forge test --mt test_sellVotes_DoestChange_ParticipantStatus -vv

Proof Of Code:

    function test_sellVotes_DoestChange_ParticipantStatus() public {
		address alice = makeAddr("alice");
		uint256 profileId  = createNewMarket(user);
		uint256 maxVotesToBuy = 10;
		uint256 minVotesToBuy = 0;
		uint256 minPrice = 0;
		// -- Buying Votes --
		buyVotes(alice, profileId, maxVotesToBuy, minVotesToBuy, 0);
		ReputationMarket.MarketInfo memory marketData = market.getUserVotes(alice, profileId);
		bool status_before = market.isParticipant(profileId, alice);
		// -- Asserting --
		assertGt(marketData.trustVotes, 0);
		assertGt(marketData.distrustVotes, 0);
		assertEq(status_before, true);
		console.log("Alice trust votes: ", marketData.trustVotes);
		console.log("Alice distrust votes: ", marketData.distrustVotes);
		console.log("Alice status before selling votes: ", status_before);
		// -- Selling Votes --
		sellVotesTrust(alice, profileId, maxVotesToBuy, minPrice);
		sellVotesDisTrust(alice, profileId, maxVotesToBuy, minPrice);
		ReputationMarket.MarketInfo memory marketDataAfter = market.getUserVotes(alice, profileId);
		// -- Asserting --
		assertEq(marketDataAfter.trustVotes, 0);
		assertEq(marketDataAfter.distrustVotes, 0);
		bool status_after = market.isParticipant(profileId, alice);
		console.log("Alice trust votes after selling: ", marketDataAfter.trustVotes);
		console.log("Alice distrust votes after selling: ", marketDataAfter.distrustVotes);
		console.log("Alice status after selling votes: ", status_after);
    }

Output:

[PASS] test_sellVotes_DoestChange_ParticipantStatus() (gas: 1210659)
  Alice trust votes:  10
  Alice distrust votes:  10
  Alice status before selling votes:  true
  Alice trust votes after selling:  0
  Alice distrust votes after selling:  0
  Alice status after selling votes:  true

Mitigation

Add participant status reset in sellVotes function when user sells all their votes. Here is the recommended mitigation:

function sellVotes(...) {
    // existing code...

    votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesToSell;

    // Add check for remaining votes and reset participant status if none left
+    if (votesOwned[msg.sender][profileId].votes[TRUST] == 0 &&
+        votesOwned[msg.sender][profileId].votes[DISTRUST] == 0) {
+        isParticipant[profileId][msg.sender] = false;
+    }

    // rest of existing code...
}