Skip to content

Latest commit

 

History

History
109 lines (71 loc) · 3.4 KB

File metadata and controls

109 lines (71 loc) · 3.4 KB

Melodic Latte Pheasant

High

Wrong rounding direction will cause initial liquidity drained

Summary

The wrong rounding direction in _calcCost() will drain market liquidity during regular trading operations.

Root Cause

In ReputationMarket.sol:1057 rounding direction is determined based on the value of isPositive flag. The mistake is that isPositive is true for the TRUST votes and false for DISTRUST votes. That means that rounding direction is dictated by the vote's type the _calCost() function processes at the moment, which is wrong. Rounding must always favor the protocol: round up when a user buys votes, and round down when a user sells votes back.

The statements below illustrate current _calcCost() behaviour:

  • User buys TRUST votes => Rounding.Floor (WRONG: less cost paid)
  • User sells TRUST votes => Rounding.Floor (FINE)
  • User buys DISTRUST votes => Rounding.Ceil (FINE)
  • User sells DISTRUST votes => Rounding.Ceil (WRONG: higher cost refunded)

Internal Pre-conditions

None

External Pre-conditions

None

Attack Path

Users buy and sell votes.

Impact

The reputation market suffers from a liquidity drain.

The following invariant is broken:

The contract must never pay out the initial liquidity deposited as part of trading.

PoC

Insert this test into ethos/packages/contracts/test/reputationMarket/rep.market.test.ts

  it('poc_01', async () => {
    const profileId = ethosUserB.profileId;

    await reputationMarket
      .connect(deployer.ADMIN)
      .addMarketConfig(100n, ethers.parseEther('.001'), DEFAULT.creationCost);

    const marketConfigIndex = (await reputationMarket.getMarketConfigCount()) - 1n;

    await reputationMarket.connect(deployer.ADMIN).setUserAllowedToCreateMarket(profileId, true);

    await reputationMarket
      .connect(userB.signer)
      .createMarketWithConfig(marketConfigIndex, { value: DEFAULT.creationCost });

    let funds = await reputationMarket.marketFunds(profileId);
    console.log(`Market funds (initial state)       : ${funds}`);

    // buy positive votes
    const votes = 50n;

    for (let i = 0; i < votes; i++) {
      await userA.buyVotes({ profileId, votesToBuy: 1n });
      funds = await reputationMarket.marketFunds(profileId);
    }

    console.log(`Market funds after buying ${votes} votes : ${funds}`);

    await userA.sellVotes({ profileId, sellVotes: votes });
    funds = await reputationMarket.marketFunds(profileId);
    console.log(`Market funds after selling ${votes} votes: ${funds}`);
  });

Output:

$ npx hardhat test --grep poc_01


  ReputationMarket
Market funds (initial state)       : 1000000000000000000
Market funds after buying 50 votes : 1028092980362016112
Market funds after selling 50 votes: 999999999999999975
    ✔ poc_01 (1191ms)


  1 passing (3s)

Mitigation

In the _calcCost(), determine rounding direction based on costRatio sign:

    uint256 positiveCostRatio = costRatio > 0 ? uint256(costRatio) : uint256(costRatio * -1);
    // multiply cost ratio by base price to get cost; divide by 1e18 to apply ratio
    cost = positiveCostRatio.mulDiv(
      market.basePrice,
      1e18,
--    isPositive ? Math.Rounding.Floor : Math.Rounding.Ceil
++    costRatio < 0 ? Math.Rounding.Floor : Math.Rounding.Ceil
    );