Rain Disclosed Report

Permanent Denial of Service (DoS) in `RainPool` Settlement Caused by `platformShare` Inflation After Sales Period

Company
Created date
hidden

Target

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

Vulnerability Details

Description: The RainPool contract is vulnerable to a permanent Denial of Service (DoS) that prevents the settlement of any pool, permanently locking all user and liquidity provider funds. The vulnerability stems from an accounting error in the enterOption() function when called after the sales period has ended (block.timestamp > endTime) but before the pool is finalized (closePool() has not been called).

Reproduction Steps:

  1. Precondition: A RainPool contract is in its "post-sale, pre-settlement" state. This is a natural window in every pool's lifecycle where block.timestamp > endTime and winner == 0.
  2. Attack Execution: An attacker, who can be any external owned account (EOA) without special privileges, calls the enterOption(uint256 option, uint256 amount) function. The amount is set to a large value, significantly greater than the total funds (allFunds) currently in the pool.
  3. Internal Mechanism:
    • Inside enterOption(), a platform fee (fee) is calculated based on the attacker's amount and is immediately added to the platformShare state variable (L403).
    • Because the sales period has ended, the function enters a specific logic branch (L447-496) that only attempts to match orders from the on-chain order book. In this stage, the AMM logic which would increase allFunds is bypassed.
    • Assuming the order book is empty (which is common after the sale ends), no trades are executed.
    • The function then reaches its end and refunds the entire original amount back to the attacker (L495).
  4. Resulting State Corruption: The platformShare variable has been artificially inflated with a fee that was never backed by real assets (since the principal was fully refunded). However, allFunds remains unchanged. This breaks the core accounting invariant of the pool (allFunds should equal the sum of all shares).
  5. Permanent DoS: Any subsequent attempt by any user to call closePool() will now fail. The function will revert due to an arithmetic underflow (L681-686) when it tries to calculate winningPoolShare by subtracting the artificially inflated platformShare from totalBaseTokens (allFunds). Since there is no function to decrease platformShare, the pool is permanently bricked, and all funds are locked forever.

The attack requires no capital, as the principal is fully refunded, and the only cost is the gas fee for a single transaction.

Validation steps

The vulnerability is fully validated through a Foundry Proof-of-Concept (POC) test suite.

POC File: test/POC_Finding23_PlatformShareInflation.t.sol

Logical Validation Flow: The POC simulates the exact attack scenario and confirms the outcome. The key test case for validation is test_DoS_ClosePoolRevertAfterAttack().

  1. Setup:

    • A new RainPool instance is deployed with standard parameters.
    • A "normal user" participates in the pool during the sales period by calling enterOption(), establishing a non-zero allFunds value.
    • The test environment's timestamp is advanced past the pool's endTime using vm.warp(endTime + 1) to enter the vulnerable window.
  2. Attack Simulation:

    • An "attacker" account calls enterOption() with a large amount (e.g., 2,400,000 tokens), which is calculated to be sufficient to make platformShare larger than the remaining funds after other fees are accounted for.
  3. State Verification:

    • The POC asserts that after the attacker's transaction, platformShare has increased, while allFunds and the attacker's token balance remain unchanged. This confirms the accounting imbalance.
    • Log Output from test_Attack_InflatePlatformShareAfterEndTime():
      After attack enterOption call:
        Attacker balance:  10000000000000 (Unchanged - full refund)
        platformShare:     21250250000    (Increased by 20B)
        allFunds:          50010000000    (Unchanged)
      
  4. DoS Confirmation:

    • The POC then attempts to call closePool().
    • It uses Foundry's vm.expectRevert() to assert that this call must fail with an arithmetic underflow.
    • Log Output from test_DoS_ClosePoolRevertAfterAttack():
      State before closePool attempt:
        allFunds:        50010000000
        platformShare:   49250250000 (Inflated to 98.5% of allFunds)
      
      Attempting to call closePool...
      RESULT: closePool reverted as expected (arithmetic underflow)
        poolFinalized:  false
      
      DoS CONFIRMED: Pool is permanently locked
      
  5. Execution:

    • Running the full test suite via forge test --match-contract POC_Finding23_PlatformShareInflation results in all 6 tests passing, which comprehensively confirms the vulnerability, its boundary conditions, and the DoS impact.

Attachments

hidden
CommentsReport History
Comments on this report are hidden
Details
State
hidden
Severity
Critical
Bounty$48
Visibilitypartially
VulnerabilityBlockchain
Participants
hidden