Skip to content

Latest commit

 

History

History
226 lines (139 loc) · 7.32 KB

File metadata and controls

226 lines (139 loc) · 7.32 KB

Glamorous Canvas Camel

Medium

Contract Balance May Become Less Than Total Active Market Funds Due to Fee Transfers

Summary

The ReputationMarket contract violates the invariant that "Total contract balance must be greater or equal to all active (non-graduated) market funds." This issue arises because protocol fees and donations are immediately transferred out of the contract during buy and sell operations, reducing the contract's ETH balance without a corresponding decrease in marketFunds. As a result, over time, the contract's balance can become less than the total marketFunds of all active markets, potentially leading to scenarios where the contract cannot fulfill its obligations to users during withdrawals or market graduations.

Root Cause

Broken Invariant

From the contract's documentation:

Total contract balance must be greater or equal to all active (non-graduated) market funds


  • Immediate Transfer of Fees and Donations:

    In the applyFees function, protocol fees are immediately sent to the protocolFeeAddress, and donations are allocated to donationEscrow. This reduces the contract's ETH balance.

https://github.com/sherlock-audit/2024-12-ethos-update/blob/c3a2b007d0ddfcb476f300f8b766808f0e3e2dfd/ethos/packages/contracts/contracts/ReputationMarket.sol#L1086C1-L1097C4

  function applyFees(
      uint256 protocolFee,
      uint256 donation,
      uint256 marketOwnerProfileId
  ) private returns (uint256 fees) {
      // Allocate donation to escrow
      donationEscrow[donationRecipient[marketOwnerProfileId]] += donation;

      // Transfer protocol fees immediately
      if (protocolFee > 0) {
          (bool success, ) = protocolFeeAddress.call{ value: protocolFee }("");
          if (!success) revert FeeTransferFailed("Protocol fee deposit failed");
      }
      fees = protocolFee + donation;
  }
  • Market Funds Not Adjusted for Fees:

    In the buyVotes and sellVotes functions, marketFunds is adjusted by purchaseCostBeforeFees or proceedsBeforeFees, which do not include fees or donations. Therefore, marketFunds does not account for ETH leaving the contract due to fees.

    // In buyVotes:
    marketFunds[profileId] += purchaseCostBeforeFees;
    
    // In sellVotes:
    marketFunds[profileId] -= proceedsBeforeFees;
  • Contract Balance Reduction Without Corresponding Adjustment:

    As fees and donations are transferred out, the contract's ETH balance decreases. However, since marketFunds is not reduced by these amounts, the total marketFunds can exceed the contract's ETH balance, violating the invariant.

Internal Pre-conditions

No response

External Pre-conditions

No response

Attack Path

While this issue may not be directly exploitable by a malicious actor to drain funds, it can lead to a scenario where the contract is unable to meet its obligations due to internal accounting inconsistencies.

Scenario Steps:

  1. Users Buy Votes with Fees and Donations:

    • A user buys votes:

      • purchaseCostBeforeFees: 10 ETH
      • Protocol fee (e.g., 5%): 0.5 ETH
      • Donation (e.g., 2%): 0.2 ETH
      • totalCostIncludingFees: 10.7 ETH
    • marketFunds increases by purchaseCostBeforeFees (10 ETH).

    • The contract's ETH balance increases by totalCostIncludingFees (10.7 ETH).

    • Protocol fee and donation are immediately transferred out:

      applyFees(protocolFee, donation, profileId);
      • Contract's ETH balance net increase: 10.7 ETH - 0.5 ETH (protocol fee) = 10.2 ETH
      • Donation is allocated to donationEscrow, reducing available funds for market obligations.
  2. Repeated Transactions Reduce Available Balance:

    • Repeating such transactions, the contract's ETH balance continues to decrease relative to marketFunds due to the immediate transfer of fees and allocation of donations.
  3. Violation of Invariant:

    • Over time, the total marketFunds can exceed the contract's ETH balance.

      • Example:

        • Total marketFunds: 200 ETH
        • Contract's ETH balance: 180 ETH (due to 20 ETH in protocol fees and donations transferred out)
    • Contract balance (180 ETH) < Total marketFunds (200 ETH)

Impact

No response

PoC

Setup

  • Initial State:

    • Contract's ETH Balance: 100 ETH
    • marketFunds[Market A]: 100 ETH
    • Protocol fees and donations are not yet collected.

Transaction 1: User Buys Votes

  • User Buys Votes in Market A:

    // Variables
    uint256 purchaseCostBeforeFees = 10 ETH;
    uint256 protocolFee = 0.5 ETH; // 5% of 10 ETH
    uint256 donation = 0.2 ETH;    // 2% of 10 ETH
    uint256 totalCostIncludingFees = 10 ETH + 0.5 ETH + 0.2 ETH = 10.7 ETH;
  • Contract State Changes:

    • marketFunds[Market A] increases by purchaseCostBeforeFees (10 ETH):

      marketFunds[marketAProfileId] += purchaseCostBeforeFees; // +10 ETH
    • Contract's ETH balance increases by totalCostIncludingFees (10.7 ETH):

      // User sends 10.7 ETH to the contract
    • Protocol fee and donation are transferred out or allocated:

      applyFees(protocolFee, donation, marketAProfileId);
      • Contract's ETH balance decreases by protocol fee (0.5 ETH):

        // Send 0.5 ETH to protocolFeeAddress
      • Donation (0.2 ETH) is allocated to donationEscrow (not immediately affecting ETH balance but reserved).

    • Net Changes:

      • Contract's ETH balance net increase: 10.7 ETH - 0.5 ETH = 10.2 ETH
      • marketFunds[Market A] increased by 10 ETH
      • donationEscrow increased by 0.2 ETH (reserved funds)

Contract Balance vs. Market Funds

  • After Transaction 1:

    • marketFunds[Market A]: 110 ETH (initial 100 ETH + 10 ETH)
    • Contract's ETH balance: 110.2 ETH (initial 100 ETH + net increase 10.2 ETH)
    • Contract Balance (110.2 ETH) >= marketFunds (110 ETH)
  • However, 0.2 ETH in donationEscrow is reserved and effectively reduces available ETH.

  • Available ETH Balance: 110.2 ETH - 0.2 ETH (donations) = 110 ETH

Repeated Transactions

  • Over Time:

    • Protocol fees and donations continue to be transferred out or allocated.
    • Contract's ETH balance grows at a slower rate than marketFunds due to fees being sent out.
  • Example After Multiple Transactions:

    • Total marketFunds across all markets: 500 ETH
    • Contract's ETH balance: 480 ETH
    • Donations in donationEscrow: 10 ETH
    • Available ETH Balance: 480 ETH - 10 ETH = 470 ETH
  • Result:

    • Available ETH Balance (470 ETH) < Total marketFunds (500 ETH)
    • Invariant Violated: Contract balance is less than total marketFunds

Mitigation

To maintain the invariant and ensure that the contract has sufficient funds to meet its obligations, adjust marketFunds to account for protocol fees and donations that are transferred out or allocated.

  • Subtract Fees from marketFunds During Transactions:

    • In buyVotes:

      // Adjust marketFunds by the net amount after fees and donations
      marketFunds[profileId] += purchaseCostBeforeFees - (protocolFee + donation);
    • In sellVotes:

      // Adjust marketFunds by the net amount after fees
      marketFunds[profileId] -= proceedsBeforeFees - protocolFee;