Skip to content

Latest commit

 

History

History
124 lines (92 loc) · 3.81 KB

File metadata and controls

124 lines (92 loc) · 3.81 KB

Orbiting Steel Tardigrade

High

Missing slippage protection for the sellVotes() function

Summary

In the buyVotes() function, the protocol calls the _checkSlippageLimit() function to implement slippage protection.

 function buyVotes(
    uint256 profileId,
    bool isPositive,
    uint256 expectedVotes,
    uint256 slippageBasisPoints
  ) public payable whenNotPaused {
    _checkMarketExists(profileId);
    _checkAddressHasProfile();

    // determine how many votes can be bought with the funds provided
    (uint256 votesBought, uint256 fundsPaid, ) = _calculateBuy(
      markets[profileId],
      isPositive,
      msg.value
    );

    _checkSlippageLimit(votesBought, expectedVotes, slippageBasisPoints);

However, in the sellVotes() function, there are no slippage protection measures in place, making it vulnerable to sandwich attacks during the selling process.

function sellVotes(uint256 profileId, bool isPositive, uint256 amount) public whenNotPaused {
    _checkMarketExists(profileId);
    _checkAddressHasProfile();

    // calculate the amount of votes to sell and the funds received
    (uint256 votesSold, uint256 fundsReceived, ) = _calculateSell(
      markets[profileId],
      profileId,
      isPositive,
      amount
    );

    // update the market state
    markets[profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold;
    votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold;

    // Check if seller has no votes left after selling
    if (
      votesOwned[msg.sender][profileId].votes[TRUST] == 0 &&
      votesOwned[msg.sender][profileId].votes[DISTRUST] == 0
    ) {
      isParticipant[profileId][msg.sender] = false;
    }

    // send the proceeds to the seller
    payable(msg.sender).transfer(fundsReceived);
    emit VotesSold(profileId, msg.sender, isPositive, votesSold, fundsReceived, block.timestamp);
    _emitMarketUpdate(profileId);
  }

Root Cause

In the sellVotes() function, there are no slippage protection measures https://github.com/sherlock-audit/2024-10-ethos-network/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L233-L261

Internal pre-conditions

No response

External pre-conditions

No response

Attack Path

  1. A regular user prepares to call the sellVotes() function to sell their votes.
  2. A malicious user notices the regular user's intent and sells their votes in advance, driving down the price. After the regular user sells, due to the lack of slippage protection, the selling result is unfavorable. The regular user then buys back their votes at a lower price, effectively obtaining more votes.

Impact

The result of the user's vote sale is not as expected.

PoC

function sellVotes(uint256 profileId, bool isPositive, uint256 amount) public whenNotPaused {
    _checkMarketExists(profileId);
    _checkAddressHasProfile();

    // calculate the amount of votes to sell and the funds received
    (uint256 votesSold, uint256 fundsReceived, ) = _calculateSell(
      markets[profileId],
      profileId,
      isPositive,
      amount
    );

    // update the market state
    markets[profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold;
    votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] -= votesSold;

    // Check if seller has no votes left after selling
    if (
      votesOwned[msg.sender][profileId].votes[TRUST] == 0 &&
      votesOwned[msg.sender][profileId].votes[DISTRUST] == 0
    ) {
      isParticipant[profileId][msg.sender] = false;
    }

    // send the proceeds to the seller
    payable(msg.sender).transfer(fundsReceived);
    emit VotesSold(profileId, msg.sender, isPositive, votesSold, fundsReceived, block.timestamp);
    _emitMarketUpdate(profileId);
  }

Mitigation

Recommended fix: add slippage protection.