Status DataClose notification

Rain Disclosed Report

Zero-Weight Options Receive Initial Votes (Claim Without Funds)

Company
Created date
hidden

Target

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

Vulnerability Details

Summary

If a liquidityPercentages[i] is 0, the constructor sets totalFunds[i] = 0 but still assigns totalVotes[i] = initialLiquidity and credits userVotes[i][poolOwner] with a large balance. This creates options with zero initial funds (price ~ 0) but large initial votes held by the creator. If such an option wins later, the creator can claim from winningPoolShare without having backed that outcome with any initial funds.

Affected Code

  • src/RainPool.sol constructor: funds per option based on percentages; votes per option set to initialLiquidity uniformly.

Impact

  • Creator can receive significant winner payouts for zero-backed options if they win.
  • Distorts fair market incentives and pricing; an option priced near 0 can still yield large claims from the initial creator votes.

Validation steps

How to Reproduce

  1. Deploy with liquidityPercentages = [100, 0] and initialLiquidity > 0.
  2. Observe totalFunds[2] = 0 but totalVotes[2] = initialLiquidity and userVotes[2][owner] = initialLiquidity.
  3. If option 2 wins, creator claims from winningPoolShare via their votes despite zero initial funds on that option.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "forge-std/Test.sol";
import {RainFactory} from "src/RainFactory.sol";
import {RainDeployer, IRainDeployer} from "src/RainDeployer.sol";
import {RainPool} from "src/RainPool.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {ERC20Mock} from "src/mocks/ERC20Mock.sol";

contract Rain021_ZeroWeightGetsVotesTest is Test {
    function test_ZeroWeightOption_ReceivesInitialVotes() public {
        RainFactory factory = new RainFactory();
        address owner = makeAddr("owner");
        address resolverAI = makeAddr("resolverAI");
        ERC20Mock base = ERC20Mock(0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9);
        deal(address(base), address(this), 1e15);
        RainDeployer impl = new RainDeployer();
        bytes memory init = abi.encodeWithSelector(
            RainDeployer.initialize.selector,
            address(factory), address(this), address(base), owner, resolverAI,
            address(0x25118290e6A5f4139381D072181157035864099d),
            address(0xE592427A0AEce92De3Edee1F18E0157C05861564),
            6, 12, 25, 15 * 1e6, 12, 1
        );
        RainDeployer dep = RainDeployer(address(new ERC1967Proxy(address(impl), init)));

        uint256[] memory w = new uint256[](2); w[0]=100; w[1]=0;
        IRainDeployer.Params memory p = IRainDeployer.Params({
            isPublic: true, resolverIsAI: true, poolOwner: owner,
            startTime: block.timestamp + 1, endTime: block.timestamp + 100,
            numberOfOptions: 2, oracleEndTime: block.timestamp + 200,
            ipfsUri: "look@zero", initialLiquidity: 10_000_000,
            liquidityPercentages: w, poolResolver: owner
        });
        base.approve(address(dep), p.initialLiquidity + dep.oracleFixedFee());
        RainPool pool = RainPool(dep.createPool(p));

        assertEq(pool.totalFunds(2), 0, "zero-weight option has funds");
        assertEq(pool.totalVotes(2), p.initialLiquidity, "zero-weight option did not get initial votes");
        assertEq(pool.userVotes(2, owner), p.initialLiquidity, "owner not credited votes on zero-weight option");
    }
}


Attachments

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