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

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

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. Contracts are deployed and functional on Arbitrum One but have not undergone an external audit. Size positions accordingly.