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.
Rain Vaults lets anyone run a trading strategy on Rain prediction markets with pooled capital, Hyperliquid-style:
| Item | Address |
|---|---|
| Network | Arbitrum 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 RPC | https://arb1.arbitrum.io/rpc |
| Role | Can do | Cannot 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 markets | Withdraw anything, authorize other keys, close the vault |
| Depositor | Deposit, request withdrawal (after lock-up), cancel own queue request, force-unwind after grace, crank fees/queue via poke() | Trade |
| Anyone | claim() resolved winnings for the vault, processQueue(), forceUnwind() when conditions are met | — |
USDT.approve(factory, seedAmount). Minimum seed: 100 USDT (MIN_SEED = 100e6).createVault(name, description, performanceFeeBps, managementFeeBps, lockupPeriod, seedAmount):| Param | Type / limits | Meaning |
|---|---|---|
| name | string, immutable | Vault display name |
| description | string, immutable | Strategy description shown to depositors |
| performanceFeeBps | 0–5000 (= 0–50%) | % of profit above high-water mark, paid to manager |
| managementFeeBps | 0–500 (= 0–5%/yr) | Annual fee on assets, accrued per second |
| lockupPeriod | 0–2592000 sec (0–30 d) | Per-deposit withdrawal lock |
| seedAmount | ≥ 100e6 | Manager'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.
USDT.approve(vault, amount);
vault.deposit(amount); // credits msg.sender
// or vault.depositFor(beneficiary, amount);
amount × totalShares / NAV (first deposit: 1 share per USDT-wei, plus 1000 dead shares as an inflation-attack guard).sharePrice = NAV / totalShares (1e18 scale).firstBuyOrderPrice) + full claim value of resolved options. Conservative marking protects existing depositors from overpriced entries.Vault page → Manager Panel (visible when connected as manager): register market, place/cancel orders.
setOperator(address, true).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
GET https://prod2-api.rain.one/pools/public-pools?limit=30 (field contractAddress).490000000000000000. Tick spacing 0.01.100000000.firstBuyOrderPrice(option, side) / firstSellOrderPrice(option, side) on the market contract.tradingModel() → 0 = AMM (buy/split/merge only, no sellOption before resolution), 1 = OrderBook (full limit-order trading).claim after resolution). Prices ≈ implied probability.poke().Three paths, fastest first:
requestWithdraw(shares) — if the vault has enough free USDT and your lock-up passed, you're paid immediately at current NAV.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.LockupActive.ManagerBelowMinimum).cancelWithdrawRequest(index).closeVault() — accrues final fees, freezes trading.withdrawAfterClose(shares) — no lock-up, no queue.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)
setOperator(address operator, bool enabled) // authorize/revoke trading keys
closeVault()
See section 6b.
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
createVault(string name, string desc, uint256 perfBps, uint256 mgmtBps, uint256 lockup, uint256 seed) → address
getAllVaults() → address[]
vaultsByManager(address, uint256) → address
MIN_SEED() → uint256 // 100e6
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)
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);
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);
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
);
await vault.tradeSellOption(MARKET_ADDR, 1, 1, ethers.parseUnits('100', 6), 0);
const [name, manager, nav, shares, price] = await vault.vaultInfo();
const myPos = await market.userOptionPerSideShares(1, 1, VAULT_ADDR); // vault's YES shares
await vault.claimResolved(MARKET_ADDR, 1);
baseToken() is USDT — the vault cannot approve arbitrary contracts.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:
| Area | v2/v3 | V4 rev 4.1 |
|---|---|---|
| Deposit | deposit(amount) mints at NAV | deposit(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. |
| Withdraw | requestWithdraw(shares) → instant or FIFO queue | withdraw(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 remainder | n/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-unwind | forceUnwind(idx, side, minOut) after 48 h queue wait | forceUnwind(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[]). |
| Markets | registerMarket by manager or key | Manager-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 views | nav(), sharePrice() (marked) | No NAV. cash() = shareholders’ realised USDT; sharePrice() = cash / totalShares; isFlat(); freeShares(market, option, side); vaultInfo(). |
| Fees | Continuous on NAV | Only on realised cash at flat moments, above high-water mark. Management fee crystallised at the start of every withdraw. |
| Capacity | Unbounded | Effective 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.