Rain Disclosed Report

Critical Accounting Flaw in closePool() Causes Permanent DoS via platformShare Underflow

Company
Created date
hidden

Target

https://github.com/hackenproof-public/rain-contracts

Vulnerability Details

Summary

The RainPool contract contains a critical accounting flaw in the closePool() function that leads to arithmetic underflow when orderbook trading volume reaches approximately 38-40x the AMM pool size. This results in permanent denial of service, with all pool funds (potentially millions of dollars) locked forever with no recovery mechanism.

Root Cause

The closePool() function at lines 681-686 incorrectly assumes that all platformShare fees originated from allFunds:

winningPoolShare = totalBaseTokens - platformShare - liquidityShare - creatorShare - resolverShare;

The flawed assumption:

platformShare ⊆ allFunds

Reality:

platformShare = AMM_fees + Orderbook_fees
allFunds = AMM_deposits_only

Technical Analysis

  1. Fund Tracking Disparity:

    • allFunds ONLY tracks AMM deposits via enterOption() (lines 508, 537) and enterLiquidity() (line 637)
    • platformShare accumulates from TWO sources:
      • AMM fees (lines 355, 403, 624) - These ARE part of allFunds ✓
      • Orderbook execution fees (lines 1679, 1757) - These are NOT part of allFunds ✗
  2. The Critical Path - placeBuyOrder(): When users call placeBuyOrder() (lines 1063-1171):

    // Line 1083: External capital enters
    IERC20(baseToken).safeTransferFrom(msg.sender, address(this), amount);
    
    // Lines 1100-1107: Executes against sell orders
    (usedAmount, sharesReceived) = _executeSellOrder(...);
    amount -= usedAmount;
    
    // Line 1155: Remaining goes to escrow, NOT allFunds
    userAmountInEscrow[option][msg.sender] += amount;
    

    Inside _executeSellOrder() (lines 1666-1693):

    uint256 fee = (userAmount * ORDER_EXECUTION_FEE) / FEE_MAGNIFICATION; // 2.5%
    platformShare += fee;  // Line 1679 - Adds fee WITHOUT adding to allFunds
    IERC20(baseToken).safeTransfer(sellerAddress, userAmount); // Seller paid immediately
    
  3. The Underflow Condition: In closePool():

    uint256 totalBaseTokens = allFunds;  // Line 672
     liquidityShare = (totalBaseTokens * 1.2%) / 1000;
     creatorShare = (totalBaseTokens * 1.2%) / 1000;
     resolverShare = (totalBaseTokens * 0.1%) / 1000;
     
     // Line 681-686: UNDERFLOW when platformShare + other fees > allFunds
     winningPoolShare = totalBaseTokens - platformShare - liquidityShare - creatorShare - resolverShare;
    

Mathematical threshold:

Underflow when: platformShare + (2.5% of allFunds) > allFunds
Simplifies to: platformShare > 97.5% of allFunds
Required orderbook/AMM ratio: ~38-40x

Impact Assessment

  1. Permanent Denial of Service
    • closePool() reverts with arithmetic underflow
    • Function becomes permanently inaccessible
    • No recovery or rescue mechanism exists
  2. Total Fund Lockup
    • All winning option holders cannot claim rewards
    • All liquidity providers cannot withdraw funds
    • Pool creator cannot claim creator fees
    • Resolver cannot claim resolver fees
    • Platform cannot collect platform fees
  3. Economic Loss
    • Entire pool TVL permanently frozen
    • Potential losses: Millions of dollars in popular pools
    • No admin function to rescue funds
  4. Attack Vectors
    • Organic: High-volume prediction markets naturally reach 38-40x orderbook/AMM ratios
    • Malicious: Deliberate griefing attack
      • Cost to attacker: ~3.7% of orderbook volume
      • To brick a $10,000 pool: ~$14,000 attack cost
      • Griefing ratio highly favorable for attacker
  5. Affected Parties
    • Option holders (winners)
    • Liquidity providers
    • Pool creators
    • Resolvers
    • Platform (loss of fees and reputation)

Validation steps

Step 1: Setup POC Test File

Save the following test as test/PlatformShareUnderflowFinal.t.sol in your Foundry project.

Step 2: Run the Accounting Mismatch Demo

forge test --match-test test_ProofOfAccountingMismatch \
  --fork-url https://arb-mainnet.g.alchemy.com/v2/{api_key} \
  -vv

Expected Output:

BEFORE placeBuyOrder():
  allFunds:      $110000
  platformShare: $2750

AFTER placeBuyOrder($50000):
  allFunds:      $110000  ← NO CHANGE
  platformShare: $4000    ← INCREASED by $1250

KEY FINDING:
  * $50,000 entered contract via placeBuyOrder()
  * allFunds did NOT increase
  * platformShare DID increase by $1250

CONCLUSION:
  platformShare accumulates fees from funds NOT in allFunds!

Step 3: Run the Complete Underflow Attack POC

forge test --match-test test_ProofOfUnderflowAttack \
  --fork-url https://arb-mainnet.g.alchemy.com/v2/{api_key} \
  -vv

Expected Output:

INITIAL STATE:
  AMM Pool (allFunds): $10000
  platformShare:       $250

PHASE 1: Creating Orderbook Liquidity
  100 sellers created
  allFunds now:      $192673
  platformShare now: $495683

PHASE 2: Checking Current State
  platformShare/allFunds ratio: 257%
  
  [!] WARNING: platformShare ALREADY EXCEEDS allFunds!
  platformShare: $495683
  allFunds:      $192673
  Excess:        $303009

PHASE 3: Attempting closePool()
  closePool() calculation:
    allFunds:        $192673
    platformShare:   $495683
    Total to subtract: $500499
    Available (allFunds): $192673
  
  [X] UNDERFLOW!
    Attempting to subtract $500499 from $192673

VULNERABILITY CONFIRMED
  closePool() REVERTED with arithmetic underflow

  IMPACT:
   * Pool permanently bricked
   * All funds locked forever
   * No recovery mechanism

Step 4: Verification

The POC definitively proves:

  1. ✅ placeBuyOrder() increases platformShare without increasing allFunds
  2. ✅ With sufficient orderbook volume, platformShare exceeds allFunds
  3. ✅ closePool() attempts arithmetic: allFunds - platformShare - otherFees
  4. ✅ Results in underflow: trying to subtract $500,499 from $192,673
  5. ✅ Permanent revert in Solidity 0.8+ (no recovery possible)

Recommended Remediation

Option 1: Track Orderbook Fees Separately

uint256 public platformShareAMM;
uint256 public platformShareOrderbook;

// In closePool():
winningPoolShare = totalBaseTokens - platformShareAMM - liquidityShare - creatorShare - resolverShare;

Option 2: Include Orderbook Funds in allFunds

// In placeBuyOrder():
allFunds += amount;

// In _executeSellOrder()/_executeBuyOrder():
// Deduct payments from allFunds when sellers are paid

Option 3: Comprehensive Accounting Refactor

  • Maintain separate accounting for AMM vs Orderbook capital
  • Track total contract balance separately
  • Reconcile all fee sources against actual holdings

Attachments

hidden
CommentsReport History
Comments on this report are hidden
Details
State
hidden
Severity
Low
Bounty$114
Visibilitypartially
VulnerabilityDoS with (Unexpected) revert
Participants
hidden