Cool Mango Bobcat
Medium
The ReputationMarket contract contains a critical vulnerability in its market creation mechanism that allows malicious actors to front-run legitimate market creation transactions. This enables attackers to hijack market creation for any profile ID, leading to both financial losses and reputational damage.
The root cause lies in the createMarketWithConfig
function, which lacks any front-running protection mechanisms. Since market creation is tied to profile IDs and the first transaction to create a market for a profile ID becomes the authoritative market, attackers can observe pending transactions in the mempool and front-run them with higher gas prices.
The vulnerability creates a cascade of negative effects on the system. When an attacker front-runs a legitimate market creation, they gain control of the market's initial state and subsequent donation flows. The legitimate profile owner not only loses gas fees from their failed transaction but also loses control over their reputation market. This enables market manipulation and misappropriation of donations, while causing reputational damage as the manipulated market misrepresents the profile owner's standing.
Here's the critical vulnerable check that enforces this "first transaction wins" behavior:
// First, in _createMarket function:
function _createMarket(
uint256 profileId,
address recipient,
uint256 marketConfigIndex
) private nonReentrant {
// This is the critical check that makes front-running possible
if (markets[profileId].votes[TRUST] != 0 || markets[profileId].votes[DISTRUST] != 0)
revert MarketAlreadyExists(profileId);
// Once past this check, the first transaction sets up the market
markets[profileId].votes[TRUST] = 1;
markets[profileId].votes[DISTRUST] = 1;
markets[profileId].basePrice = marketConfigs[marketConfigIndex].basePrice;
markets[profileId].liquidityParameter = marketConfigs[marketConfigIndex].liquidity;
donationRecipient[profileId] = recipient;
// ...
}
The vulnerability stems from two aspects:
- The existence check that only looks at the vote counts:
if (markets[profileId].votes[TRUST] != 0 || markets[profileId].votes[DISTRUST] != 0)
- The immediate initialization of the market state:
markets[profileId].votes[TRUST] = 1;
markets[profileId].votes[DISTRUST] = 1;
When a front-runner observes a legitimate market creation transaction in the mempool, they can submit their own transaction with higher gas, pass this existence check first, and establish their transaction as the authoritative market creator. The legitimate user's transaction will then fail at the existence check since the votes are no longer zero.
This implementation inherently creates a race condition where whoever gets their transaction mined first becomes the market creator, regardless of their legitimacy or relationship to the profile ID.
Exploitation Scenario:
-
Alice is a reputable NFT artist with profile ID 1234, preparing to launch her reputation market to build trust with collectors.
-
Alice submits a transaction to create her market:
createMarketWithConfig(0)
- value: 0.2 ETH (creation cost)
- gas price: 50 gwei
- Mallory, monitoring the mempool, spots Alice's pending transaction and:
- Sees it's for profile ID 1234
- Notes the 0.2 ETH creation cost
- Observes the gas price
- Mallory immediately submits:
createMarketWithConfig(0)
- value: 0.2 ETH
- gas price: 55 gwei
-
Mallory's higher gas price ensures their transaction is mined first, creating a market for Alice's profile ID that Mallory controls.
-
Alice's transaction reverts with MarketAlreadyExists.
-
Mallory now:
- Controls Alice's reputation market
- Receives all donation fees from market activity
- Can manipulate initial market sentiment
- Has effectively hijacked Alice's on-chain reputation mechanism
The attack succeeds because the first transaction to create a market for a profile ID becomes the authoritative market, with no verification of the creator's relationship to the profile.
- Implement a commit-reveal scheme for market creation:
struct MarketCreationCommit {
bytes32 commitment;
uint256 timestamp;
}
mapping(address => MarketCreationCommit) public marketCreationCommitments;
function commitMarketCreation(bytes32 commitment) external {
marketCreationCommitments[msg.sender] = MarketCreationCommit({
commitment: commitment,
timestamp: block.timestamp
});
}
function createMarketWithConfig(
uint256 configIndex,
bytes32 salt
) external payable {
// Verify commitment exists and matches
MarketCreationCommit memory commit = marketCreationCommitments[msg.sender];
require(commit.timestamp + 24 hours >= block.timestamp, "Commitment expired");
require(commit.timestamp + 5 minutes <= block.timestamp, "Wait for commit delay");
require(keccak256(abi.encodePacked(msg.sender, configIndex, salt)) == commit.commitment,
"Invalid commitment");
// Proceed with market creation
...
}
- Alternative: Implement signature-based authorization:
function createMarketWithConfig(
uint256 configIndex,
uint256 deadline,
bytes memory signature
) external payable {
require(block.timestamp <= deadline, "Signature expired");
require(isValidSignature(msg.sender, configIndex, deadline, signature),
"Invalid signature");
// Proceed with market creation
}
- Alternative: Implement time-delayed market creation:
mapping(uint256 => address) public pendingMarketCreations;
function initiateMarketCreation(uint256 configIndex) external {
uint256 profileId = _getProfileIdForAddress(msg.sender);
pendingMarketCreations[profileId] = msg.sender;
}
function finalizeMarketCreation(uint256 configIndex) external payable {
uint256 profileId = _getProfileIdForAddress(msg.sender);
require(pendingMarketCreations[profileId] == msg.sender, "Not initiated");
require(block.timestamp >= marketCreationDelay, "Wait for delay");
// Proceed with market creation
}