Rain Vaults Documentation

Non-custodial strategy vaults on the Rain prediction-market protocol. Written for professional traders, market makers, bots, and AI agents — everything needed to create, fund, manage, trade, and exit vaults, fully on-chain.

Contents 1. System overview & guarantees 2. Contract addresses & network 3. Roles: manager, depositor, trading key 4. Creating a vault 5. Depositing & shares math 6. Trading (UI, bots, AI agents) 7. Rain markets: options, sides, prices 8. Fees & high-water mark 9. Withdrawals & the fairness system 10. Closing a vault 11. Full ABI reference 12. Copy-paste recipes (ethers.js) 13. Security model & limits 14. V4 / rev 4.1 — what changes for integrators
Which version does this page describe? Sections 1–13 document the v2/v3 contracts currently deployed and used by the app. Those contracts were audited by Composable Security and the audit found design-level issues in the NAV/queue model (§5, §9). Their replacement, V4 rev 4.1, is documented in §14 and on the Security & Audit page; it will replace v2/v3 after its final audit round. Until then, read §5 and §9 as “how it works today”, not as guarantees.

1. System overview & guarantees

Rain Vaults lets anyone run a trading strategy on Rain prediction markets with pooled capital, Hyperliquid-style:

Core guarantee — non-custodial by construction: there is NO function in the vault contract that lets the manager or any trading key move USDT out to themselves. Funds leave the vault only through (a) depositor withdrawals paid to the depositor, or (b) trades on registered Rain markets where the positions belong to the vault. A stolen manager key can at worst trade badly — it can never steal.

2. Contract addresses & network

ItemAddress
NetworkArbitrum One (chainId 42161)
VaultFactory (v2, current)0x8c373dcfb227e8c00a30718a7daf0270a960f8b7
VaultFactory (v1, legacy — no trading keys)0x35ccc0a078af070c5a4203a1af43b7a99995d475
Base token — USD₮0 (USDT, 6 decimals)0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9
Rain market factory (prod)0x38B3Ba1ee001E6785224E31b3031ae96CA06C677
Public RPChttps://arb1.arbitrum.io/rpc

3. Roles

RoleCan doCannot do
Manager (vault creator)Trade on registered markets, register markets, authorize/revoke trading keys, close the vault, withdraw own shares (respecting 5% minimum)Withdraw depositor funds, change fees (immutable), drop below 5% ownership while others hold shares
Trading key / operator (bot, AI agent)Trade on registered markets, register marketsWithdraw anything, authorize other keys, close the vault
DepositorDeposit, request withdrawal (after lock-up), cancel own queue request, force-unwind after grace, crank fees/queue via poke()Trade
Anyoneclaim() resolved winnings for the vault, processQueue(), forceUnwind() when conditions are met—

4. Creating a vault

  1. Approve the factory to pull your seed: USDT.approve(factory, seedAmount). Minimum seed: 100 USDT (MIN_SEED = 100e6).
  2. Call createVault(name, description, performanceFeeBps, managementFeeBps, lockupPeriod, seedAmount):
ParamType / limitsMeaning
namestring, immutableVault display name
descriptionstring, immutableStrategy description shown to depositors
performanceFeeBps0–5000 (= 0–50%)% of profit above high-water mark, paid to manager
managementFeeBps0–500 (= 0–5%/yr)Annual fee on assets, accrued per second
lockupPeriod0–2592000 sec (0–30 d)Per-deposit withdrawal lock
seedAmount≥ 100e6Manager's own deposit (skin in the game)

The factory deploys a new RainVault, credits the seed shares to you, and lists the vault in getAllVaults(). Fees are immutable forever — displayed to every depositor.

5. Depositing & shares math

USDT.approve(vault, amount);
vault.deposit(amount);            // credits msg.sender
// or vault.depositFor(beneficiary, amount);

6. Trading — UI, bots, and AI agents

6a. Manual (web UI)

Vault page → Manager Panel (visible when connected as manager): register market, place/cancel orders.

6b. Trading keys (bots / AI agents) — the recommended way

  1. Manager opens the vault page → Manager Panel → section 3.
  2. Generate API Key — creates a fresh keypair locally in the browser and authorizes it on-chain in one step. The private key is shown once; give it to your bot.
  3. Or Authorize any existing address (your bot's wallet) via setOperator(address, true).
  4. The key needs a little ETH on Arbitrum for gas (~0.001 ETH goes a long way).
  5. Revoke instantly anytime: setOperator(address, false).

An authorized key calls the vault's trading functions directly — no manual signing, fully automated, 24/7:

// All trading functions — callable by manager OR authorized operators:
registerMarket(address market)                    // one-time per market; grants USDT approval
tradePlaceBuyOrder(market, option, side, price, amount, postOnly)   // limit buy (USDT amount)
tradePlaceSellOrder(market, option, side, price, shares, postOnly)  // limit sell (shares)
tradeEnterOption(market, option, side, amount, minSharesOut)        // taker/AMM buy
tradeSellOption(market, option, side, shares, minAmountOut)         // market-sell into bids (order-book markets)
tradeCancelBuyOrders(market, option, sides[], prices[], orderIDs[])
tradeCancelSellOrders(market, option, sides[], prices[], orderIDs[])
tradeSplit(market, option, amount)                // USDT → equal YES+NO pairs
tradeMerge(market, option, amount)                // YES+NO pairs → USDT
claimResolved(market, option)                     // anyone; claims resolved winnings to the vault

7. Rain markets: options, sides, prices

8. Fees & high-water mark

9. Withdrawals & the fairness system (v2/v3 — deployed)

The queue/NAV model below is what the live v2/v3 vaults do. The audit showed the NAV used for the instant path can be moved by a resting order on a thin book (findings C03/C04). V4 removes both the NAV and the queue — see §14.

Three paths, fastest first:

  1. Instant: requestWithdraw(shares) — if the vault has enough free USDT and your lock-up passed, you're paid immediately at current NAV.
  2. Queue: not enough free cash → your request joins a FIFO queue. Any USDT that arrives (sells, merges, claims) services the queue before new trading. The manager has a 48-hour grace period to unwind positions in an orderly way.
  3. Force-unwind (permissionless): if the head of the queue has waited > 48h, anyone can call forceUnwind(positionIndex, side, minAmountOut): cancels resting orders / claims resolved options / market-sells up to 20% of a position per call (order-book) or merges YES+NO pairs (AMM) until the queue is paid. Slippage is borne at fill time — those demanding liquidity pay its true market price; remaining depositors are not diluted.
Queued shares remain exposed to PnL until actually filled — the payout price is set at fill time, which is fair to those who stay.

10. Closing a vault

  1. Manager unwinds all positions (NAV must equal free cash).
  2. closeVault() — accrues final fees, freezes trading.
  3. Everyone (including the manager, no 5% rule anymore) exits via withdrawAfterClose(shares) — no lock-up, no queue.

11. Full ABI reference (RainVault)

Depositor functions

deposit(uint256 amount) → uint256 shares
depositFor(address beneficiary, uint256 amount) → uint256 shares
requestWithdraw(uint256 shares) → bool instant
cancelWithdrawRequest(uint256 index)
withdrawAfterClose(uint256 shares)
processQueue(uint256 maxRequests)      // permissionless crank
poke()                                  // accrue fees + process queue
forceUnwind(uint256 positionIndex, uint8 side, uint256 minAmountOut)  // after 48h grace
forceCancelOrders(uint256 positionIndex, bool buySide, uint8[] sides, uint256[] prices, uint256[] orderIDs)

Manager functions

setOperator(address operator, bool enabled)   // authorize/revoke trading keys
closeVault()

Trading functions (manager + operators)

See section 6b.

Views

vaultInfo() → (name, manager, nav, totalShares, sharePrice, perfFeeBps, mgmtFeeBps, lockup, closed)
nav() → uint256                       // USDT, 6 decimals, conservative mark
sharePrice() → uint256                // 1e18 scale
sharesOf(address) → uint256
lastDepositAt(address) → uint256      // unix; + lockupPeriod() = unlock time
lockupPeriod() → uint256
operators(address) → bool
operatorsCount() / operatorList(uint256)
allowedMarket(address) → bool
marketsCount() / markets(uint256)
trackedPositionsCount() / trackedPositions(uint256) → (market, option)
queueLength() / queueHead() / queue(uint256) → (owner, shares, requestedAt)
highWaterMark() → uint256

Factory

createVault(string name, string desc, uint256 perfBps, uint256 mgmtBps, uint256 lockup, uint256 seed) → address
getAllVaults() → address[]
vaultsByManager(address, uint256) → address
MIN_SEED() → uint256   // 100e6

Events (for indexers / analytics)

Deposit(user, amount, shares, sharePrice)
WithdrawRequested(user, shares, queueIndex)
Withdrawn(user, shares, amount, sharePrice)
MarketRegistered(market)
PositionTracked(market, option)
ManagerTrade(market, selector, option, side, amount)
PerformanceFee(manager, feeShares, newHighWaterMark)
ManagementFee(manager, feeShares)
ForceUnwind(caller, market, option, recovered)
OperatorSet(operator, enabled)
VaultClosedEvent(finalSharePrice)

12. Copy-paste recipes (ethers.js v6)

Bot setup

import { ethers } from 'ethers';
const provider = new ethers.JsonRpcProvider('https://arb1.arbitrum.io/rpc');
const key = new ethers.Wallet(process.env.TRADING_KEY, provider); // authorized via setOperator
const vault = new ethers.Contract(VAULT_ADDR, VAULT_ABI, key);

Read market prices

const market = new ethers.Contract(MARKET_ADDR, [
  'function firstBuyOrderPrice(uint256,uint8) view returns (uint256)',
  'function firstSellOrderPrice(uint256,uint8) view returns (uint256)',
], provider);
const bid = await market.firstBuyOrderPrice(1, 1); // option 1, YES — 1e18 scale
const ask = await market.firstSellOrderPrice(1, 1);

Place a limit buy ($50 on option 1 YES at 48¢)

await vault.registerMarket(MARKET_ADDR);            // once per market
await vault.tradePlaceBuyOrder(
  MARKET_ADDR,
  1,                                   // option (1-based)
  1,                                   // side: YES=1, NO=2
  ethers.parseUnits('0.48', 18),       // price, 1e18
  ethers.parseUnits('50', 6),          // USDT amount, 6 decimals
  false                                // postOnly
);

Market-sell 100 shares into the bids

await vault.tradeSellOption(MARKET_ADDR, 1, 1, ethers.parseUnits('100', 6), 0);

Check vault state

const [name, manager, nav, shares, price] = await vault.vaultInfo();
const myPos = await market.userOptionPerSideShares(1, 1, VAULT_ADDR); // vault's YES shares

Claim after resolution (anyone)

await vault.claimResolved(MARKET_ADDR, 1);

13. Security model & limits

Beta software. The deployed v2/v3 contracts were externally audited and the audit found Critical design issues that are fixed in V4, not in these deployments. Do not add new capital to v1–v3 vaults; see the risk notice. Full finding-by-finding fix map: Security & Audit.

14. V4 / rev 4.1 — what changes for integrators

V4 (“Hyperliquid in execution, not in marking”) is a full rewrite. It is not yet deployed; the factory address will be published here and on the Security page after the final audit round. Bots and agents should prepare for these changes:

Areav2/v3V4 rev 4.1
Depositdeposit(amount) mints at NAVdeposit(amount) enters a pending pool. Converts to shares at cash-per-share when the vault is flat (automatically, or via convertPending(users[]) / poke(users[])). cancelPending() any time; refundExpiredPending(user) after 14 d. A conversion that would push the manager below 5% reverts ManagerBelowMinimum.
WithdrawrequestWithdraw(shares) → instant or FIFO queuewithdraw(shares, minAmountOut) — one transaction: claims resolved positions to shared cash, merges your slice of matched YES/NO pairs, sells the remainder on Rain, measures what actually sold, reserves the rest to you. Emits Withdrawn(user, shares, amountOut, deferredPositions).
Unsold remaindern/a (queue waits)reservedOf(posKey, side, user) shares are yours. Sell later with sellReserved(market, option, side, shares, minOut), or after resolution collect with collectClaims(market, option). Reserved shares are excluded from all vault trading (ReservedShares revert).
Force-unwindforceUnwind(idx, side, minOut) after 48 h queue waitforceUnwind(idx, market, option, side, size, minOut) after 30 days without a flat state. Mismatched identity reverts StalePositionIndex. forceCancelOrders(idx, market, option, buySide, sides[], prices[], ids[]).
MarketsregisterMarket by manager or keyManager-only. Must be a Rain-factory pool, USDT base token, and expose getClaimableAmount (else MarketMissingClaimView). retireMarket(market) blocks new spend (MarketSpendRetired) while exit paths stay open.
Pricing viewsnav(), sharePrice() (marked)No NAV. cash() = shareholders’ realised USDT; sharePrice() = cash / totalShares; isFlat(); freeShares(market, option, side); vaultInfo().
FeesContinuous on NAVOnly on realised cash at flat moments, above high-water mark. Management fee crystallised at the start of every withdraw.
CapacityUnboundedEffective cap ≈ 20× the manager’s own stake (hard 5% floor, M03/M04).
// V4 rev 4.1 — key selectors for bots
withdraw(uint256 shares, uint256 minAmountOut)
deposit(uint256 amount) / depositFor(address, uint256) / cancelPending() / convertPending(address[])
sellReserved(address market, uint256 option, uint8 side, uint256 shares, uint256 minOut)
collectClaims(address market, uint256 option)
claimResolved(address market, uint256 option)                       // permissionless, guarded
forceUnwind(uint256 idx, address market, uint256 option, uint8 side, uint256 size, uint256 minOut)
registerMarket(address) / retireMarket(address)                     // manager only
tradeEnterOption / tradeSellOption / tradePlaceBuyOrder / tradePlaceSellOrder / tradeSplit / tradeMerge
cash() / sharePrice() / isFlat() / freeShares(market, option, side) / reservedOf(bytes32, uint8, address)

Source, mocks, fuzz harness and probes: contracts/v4. Fix map and methodology: Security & Audit.