https://github.com/hackenproof-public/rain-contracts
createOracle() calls:
IERC20(baseToken).approve(oracleFactoryAddress, oracleReward + fixedFee);
with no zero-first reset. Many widespread ERC-20s (e.g., USDT class) revert if you change a non-zero allowance directly to a new non-zero value. After a first successful approve/use, a second call with a different (oracleReward + fixedFee) will often revert unless you set allowance to 0 first (or use increaseAllowance/decreaseAllowance safely).
Pools call openDispute() → which forwards funds to the deployer and then calls createOracle() here.
If allowance was already non-zero and the next dispute needs a different total (very likely across pools or over time), approve can revert, making oracle creation impossible.
Net effect: disputes can’t be opened → users can’t progress the resolution track → funds can get stuck in disputed pools (RainPool’s claim() branches rely on the resolver finishing, and if disputes can’t even spin up, the system behavior diverges from spec).
Code (RainDeployer.sol):
function createOracle(
uint256 numberOfOracles,
uint256 oracleReward,
uint256 fixedFee,
address creator,
uint256 endTime,
uint256 totalNumberOfOptions,
string memory questionUri
) public returns (address) {
if (createdPools[msg.sender] != true) {
_revert(IRainDeployer.OnlyCreatedPool.selector);
}
IERC20(baseToken).approve( // unsafe for USDT-like tokens
oracleFactoryAddress,
oracleReward + fixedFee
);
address oracle = IOracle(oracleFactoryAddress).createExternalSource(
address(this),
numberOfOracles,
oracleReward,
fixedFee,
creator,
endTime,
totalNumberOfOptions,
questionUri
);
return oracle;
}
Use a USDT-style mock token (reverts on approve(newNonZero) if current allowance for the spender is non-zero).
Initialize RainDeployer with that token as baseToken and any valid oracleFactoryAddress.
Call createOracle() once with (oracleReward + fixedFee) = A → succeeds, sets allowance to A.
Call createOracle() again with a different (oracleReward + fixedFee) = B (non-zero, B != A).
Observe revert from the token approve, blocking the oracle creation and therefore blocking disputes initiated by pools.
Output: First createOracle passes.
Second createOracle reverts on approve, making dispute flow unusable until allowance is manually zeroed.
PoC Token (USDT-style) — test/mocks/RevertingApproveToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract RevertingApproveToken is IERC20 {
string public name = "MockUSDT";
string public symbol = "mUSDT";
uint8 public decimals = 6;
mapping(address => uint256) public override balanceOf;
mapping(address => mapping(address => uint256)) public override allowance;
uint256 public override totalSupply;
constructor() {
// mint plenty to the deployer for tests
balanceOf[msg.sender] = 1_000_000_000 * 1e6;
totalSupply = balanceOf[msg.sender];
}
function transfer(address to, uint256 amount) external override returns (bool) {
require(balanceOf[msg.sender] >= amount, "bal");
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
return true;
}
function approve(address spender, uint256 amount) external override returns (bool) {
// USDT-like rule: if current != 0 and amount != 0 => revert
if (allowance[msg.sender][spender] != 0 && amount != 0) revert("APPROVE_NEEDS_ZERO_FIRST");
allowance[msg.sender][spender] = amount;
return true;
}
function transferFrom(address from, address to, uint256 amount) external override returns (bool) {
require(balanceOf[from] >= amount, "bal");
uint256 alw = allowance[from][msg.sender];
require(alw >= amount, "allow");
if (alw != type(uint256).max) {
allowance[from][msg.sender] = alw - amount;
}
balanceOf[from] -= amount;
balanceOf[to] += amount;
return true;
}
}
test/Deployer_ApproveDoS.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "forge-std/Test.sol";
import {RevertingApproveToken} from "./mocks/RevertingApproveToken.sol";
import {RainDeployer} from "../src/RainDeployer.sol";
import {IOracle} from "../src/interfaces/IOracle.sol";
contract DummyOracleFactory is IOracle {
address public lastCreator;
uint256 public lastReward;
uint256 public lastFixed;
function createExternalSource(
address /*factory*/,
uint256 /*n*/,
uint256 reward,
uint256 fixedFee,
address creator,
uint256 /*endTime*/,
uint256 /*opts*/,
string calldata /*uri*/
) external override returns (address) {
lastCreator = creator;
lastReward = reward;
lastFixed = fixedFee;
return address(0xBEEF);
}
}
contract Deployer_ApproveDoS_Test is Test {
RevertingApproveToken base;
RainDeployer deployer;
DummyOracleFactory oracleFactory;
address owner = address(this);
address pool = address(0xCAFE);
function setUp() public {
base = new RevertingApproveToken();
oracleFactory = new DummyOracleFactory();
deployer = new RainDeployer();
deployer.initialize(
address(0xFACC), // _rainFactory
address(oracleFactory), // _oracleFactoryAddress
address(base), // _baseToken
address(0xPLATFORM), // _platformAddress
address(0xAI), // _resolverAI
address(0xRAIN), // _rainToken
address(0xROUTER), // _swapRouter
6, // _baseTokenDecimals
12, 25, 5_000e6, 12, 1 // fees
);
// mark pool as created so it can call createOracle()
vm.prank(owner);
(bool ok,) = address(deployer).call(abi.encodeWithSignature(
"setRainFactory(address)", address(0xDEAD)
));
assertTrue(ok);
// cheat: flip createdPools flag to simulate a valid caller
bytes32 slot = keccak256(abi.encode(pool, uint256(7))); // mapping(address=>bool) createdPools slot (approx; replace with correct if needed)
vm.store(address(deployer), slot, bytes32(uint256(1)));
// fund deployer with base (simulating RainPool -> deployer transfer)
base.transfer(address(deployer), 1_000_000e6);
}
function test_ApproveZeroFirstNeeded() public {
vm.prank(pool);
// first oracle creation with A works
deployer.createOracle(5, 100_000e6, 5_000e6, address(0xCREATOR), block.timestamp+1 days, 2, "uri");
vm.prank(pool);
// second with different sum B should revert due to USDT-style approve rule
vm.expectRevert(bytes("APPROVE_NEEDS_ZERO_FIRST"));
deployer.createOracle(5, 120_000e6, 5_000e6, address(0xCREATOR), block.timestamp+2 days, 2, "uri");
}
}