Docs Object ETFs

Object ETFs

One ERC-20 per object, backed one-to-one by the stocks that made it, created by its first buyer. A share is one dollar of the basket at creation.

you
Apple46%TSMC18%Qualcomm10%SK Hynix10%Broadcom8%8%
psIPHONE
One buy. ETH goes to the factory, the factory buys every stock, the stocks land in the object's own ETF contract, and you receive psIPHONE shares. Redeem runs the last arrow backwards.

One ERC-20 per object

Every object Photoswap can name gets its own ETF: a plain ERC-20 with 18 decimals, named after the object (Photoswap iPhone 17 Pro) with a short symbol (psIPHONE). The contract holds the stocks behind the object and nothing else. Hold one share of it and you hold a slice of every company that made the object, in the proportions of its bill of materials.

There is no pool for the ETF itself, no oracle, no manager. Its value is whatever the stocks inside it are worth. The vault shows that as NAV per share, read straight from the constituents and Robinhood mid prices.

Backed in kind

Shares are only ever created by depositing the stocks, and only ever destroyed by withdrawing them. mint(shares, to) pulls ceil(units[i] × shares / 1e18) of every constituent from the caller; redeem(shares, to) burns the shares and sends floor(units[i] × shares / 1e18) of each back. Mint rounds up and redeem rounds down, so the contract is always at least fully backed.

FunctionWhat it doesWho calls it
constituents()The stock addresses and the raw units behind 1e18 sharesAnyone (read)
previewMint(shares)Exact deposit per stock for that many sharesAnyone (read)
mint(shares, to)Pull the stocks from the caller, mint shares to `to`The factory, or anyone holding the stocks
redeem(shares, to)Burn shares, send the stocks to `to`Any holder

What a share is

A share is defined as one dollar of the basket at creation. For each stock, units_i is the raw amount that was worth weight_i dollars at the prices of that moment, through the ERC-8056 multiplier. That definition is written once and never changes.

value(raw)   = raw × multiplier / 1e36 × mid          // USD, see Vault › Share maths
units_i      = weight_i × 1e36 / (multiplier_i × mid_i)  // raw units worth weight_i dollars
NAV / share  = Σ value(units_i)                          // 1.00 at creation, drifts after
your share   = balance / totalSupply

Because the units are fixed, the ETF does not rebalance. If Apple doubles and Corning halves, the value weights drift exactly as they would in a fixed basket in your own wallet. The share count you hold never changes on its own.

Created by the first buyer

ETFs are not pre-deployed. The factory holds one implementation and creates each object's ETF as a minimal proxy on demand, so creating one costs roughly 250k gas. The frontend always calls zapMintOrCreate: if etfOf(slug) is empty it creates the ETF with the units computed from live prices and then mints; if it exists, the spec is ignored and the calldata has to match the existing constituents.

const plan = await planEtfBuy({ basket, totalUsd, ethUsd, quotes, slippageBps: 100 });
// plan.units          raw units per 1e18 shares (from the contract, or computed for a new ETF)
// plan.legs           one v4 leg per constituent, same encoding as the direct buy
// plan.expectedShares min_i( amountOut_i × 1e18 / units_i )
// plan.minShares      expectedShares × 0.99
await executeEtfBuy(walletClient, plan);   // simulate, then zapMintOrCreate{value: totalWei}

The buy is sized so every leg fills the same number of shares: for a target of totalUsd / NAV shares, leg i gets shares × value(units_i) dollars of ETH. After the swaps the factory computes min_i(balance_i × 1e18 / units_i), mints that many shares to you, and refunds any leftover stock or ETH. It never holds funds between transactions.

The slug

The key is keccak256 of the object's slug: the catalog slug for catalog objects (iphone-17-pro), or ai: plus the normalised object name for a scan. Two people scanning the same thing land in the same ETF.

Redeem

Your shares are yours to burn. The vault's Redeem pill calls redeem(balance, you) and the stocks arrive in your wallet as ordinary ERC-20s, where they show up under stocks held directly. No approval is needed and nobody can stop it.

No admin

  • No owner, no pause, no fees, no upgrade path. The implementation is initialised with a dead slug so it cannot be used directly.
  • The factory only ever sends calldata to the immutable UniversalRouter, and only holds balances for the length of one transaction.
  • A paused underlying stock (see Risks) pauses minting and redeeming of that leg, exactly as it would for the stock in your own wallet.

Addresses

Chain
Robinhood Chain · 4663
Implementation
0x618a80BE43E682816449f551Cc4Fe08F0843fD1B
Router
0x8876789976decbfcbbbe364623c63652db8c0904

Until the factory is live

The buy panel falls back to the direct swap and says so. The stocks land in your wallet; nothing else changes. Set NEXT_PUBLIC_PHOTOSWAP_FACTORY or drop contracts/deployments/mainnet.json in place and the ETF path becomes the default.