https://github.com/hackenproof-public/rain-contracts
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.
src/RainPool.sol constructor: funds per option based on percentages; votes per option set to initialLiquidity uniformly.liquidityPercentages = [100, 0] and initialLiquidity > 0.totalFunds[2] = 0 but totalVotes[2] = initialLiquidity and userVotes[2][owner] = initialLiquidity.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");
}
}