Nice Pearl Canary
High
The Liquidity Mismatch vulnerability in the ReputationMarket
smart contract arises from improper handling of ETH funds during the purchase of votes. Specifically, when users execute the buyVotes
function, the contract inaccurately tracks marketFunds[profileId]
by adding the pre-fee purchase cost without accounting for the immediately deducted fees (protocol fees and donations). Over multiple transactions, this discrepancy causes marketFunds
to reflect a higher value than the actual ETH balance held by the contract.
Impact:
- Market Lock Potential: As
marketFunds[profileId]
becomes increasingly inflated relative to the contract's real ETH balance, future attempts to sell votes (sellVotes
) may fail. This is because the contract may not possess sufficient ETH to honor the withdrawal, causing transactions to revert. - User Experience Degradation: Users attempting to sell their votes may face unexpected failures, undermining trust in the protocol's reliability.
- Financial Implications: Although funds are not directly stolen, the inability to sell votes can trap user funds within the market, leading to potential financial loss and reduced market liquidity.
While the vulnerability does not directly result in loss or theft of funds, the risk of market freeze and user inability to exit positions presents a significant operational concern, warranting a Medium severity classification.
The ReputationMarket
contract's buyVotes
function improperly updates the marketFunds
mapping by adding the gross purchase cost before fees are deducted. This oversight leads to an imbalance between the tracked marketFunds
and the actual ETH held by the contract. Over time, this mismatch can cause the contract to run out of ETH relative to the marketFunds
recorded, preventing successful executions of the sellVotes
function.
function buyVotes(
uint256 profileId,
bool isPositive,
uint256 maxVotesToBuy,
uint256 minVotesToBuy
) public payable whenNotPaused activeMarket(profileId) nonReentrant {
// ... [omitted for brevity]
// Update market state
markets[profileId].votes[isPositive ? TRUST : DISTRUST] += currentVotesToBuy;
votesOwned[msg.sender][profileId].votes[isPositive ? TRUST : DISTRUST] += currentVotesToBuy;
// Tally market funds
marketFunds[profileId] += purchaseCostBeforeFees; // Incorrect: Adds pre-fee amount
// Distribute the fees
applyFees(protocolFee, donation, profileId);
// ... [omitted for brevity]
}
Issue:
- The line
marketFunds[profileId] += purchaseCostBeforeFees;
adds the total purchase cost before deducting fees. - Subsequently,
applyFees(protocolFee, donation, profileId);
deducts fees from the contract's ETH balance but does not adjustmarketFunds
accordingly. - This results in
marketFunds[profileId]
being greater than the actual ETH available in the contract.
function testLiquidityMismatch() public {
// Let user1 create a market for their profile
vm.prank(user1);
repMarket.createMarket{value: 2 ether}();
// User2 performs multiple buys
vm.startPrank(user2);
repMarket.buyVotes{value: 1 ether}(profileIdUser1, true, 100, 50);
repMarket.buyVotes{value: 1 ether}(profileIdUser1, true, 100, 50);
vm.stopPrank();
// User2 attempts to sell votes exceeding available ETH
vm.startPrank(user2);
vm.expectRevert();
repMarket.sellVotes(profileIdUser1, true, 200, 0); // Expected to revert due to insufficient ETH
vm.stopPrank();
}
Explanation:
- Market Creation: An admin (
user1
) creates a reputation market with an initial ETH deposit. - Multiple Buys: Another user (
user2
) buys votes twice, each time sending ETH that includes fees. However,marketFunds
is incremented by the total purchase cost before fees are deducted. - Sell Attempt: When
user2
attempts to sell votes, the contract checks ifmarketFunds
can cover the proceeds. Due to the earlier mismatch, the actual ETH balance may be insufficient, causing the transaction to revert and effectively locking the market.
- Manual Review
- Foundry
Adjust marketFunds
by Net Purchase Cost