Glamorous Canvas Camel
Medium
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.
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 theprotocolFeeAddress
, and donations are allocated todonationEscrow
. This reduces the contract's ETH balance.
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
andsellVotes
functions,marketFunds
is adjusted bypurchaseCostBeforeFees
orproceedsBeforeFees
, 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 totalmarketFunds
can exceed the contract's ETH balance, violating the invariant.
No response
No response
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:
-
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 bypurchaseCostBeforeFees
(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.
-
-
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.
- Repeating such transactions, the contract's ETH balance continues to decrease relative to
-
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)
- Total
-
-
Contract balance (180 ETH) < Total
marketFunds
(200 ETH)
-
No response
-
Initial State:
- Contract's ETH Balance: 100 ETH
marketFunds[Market A]
: 100 ETH- Protocol fees and donations are not yet collected.
-
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 bypurchaseCostBeforeFees
(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 ETHdonationEscrow
increased by 0.2 ETH (reserved 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
-
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
- Total
-
Result:
- Available ETH Balance (470 ETH) < Total
marketFunds
(500 ETH) - Invariant Violated: Contract balance is less than total
marketFunds
- Available ETH Balance (470 ETH) < Total
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;
-