Brilliant Myrtle Hamster
Medium
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.
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.
- User needs to have bought votes in a market to be registered as a participant
- User needs to be marked as a participant in isParticipant mapping
- User needs to have votes to sell in votesOwned mapping
-- N/A --
- Attacker buys any amount of votes in a market using buyVotes()
- Attacker gets registered as participant in participants array and isParticipant mapping
- Attacker sells all their votes using sellVotes()
- Despite having no votes, attacker remains marked as an active participant
- getParticipantCount() continues to include the attacker in the total count
- The protocol considers the attacker an active participant for any participant-based logic
The protocol suffers from inaccurate participant accounting. This affects:
- Market statistics showing incorrect number of active participants
- Frontend displays showing misleading participant numbers to users
Creates inconsistent state and potentially affects protocol future governance/mechanics based on participant counts.
- Create a new folder and file in
test/foundry/BugTest.sol
and add the following gist code.
- To run the test, run the following command:
forge test --mt test_sellVotes_DoestChange_ParticipantStatus -vv
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);
}
[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
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...
}