Build Your First Robinhood Chain App

Share this article:

A chain becomes very interesting when there is something useful to compose with.

Robinhood Chain gives you the familiar EVM path first: Solidity contracts, Foundry deploys, viem reads, wagmi writes, ERC-20 approvals. The part worth learning is not a new VM. It is the asset layer you can build against once the contracts are live.

The demo featured here is an index basket app. A user deposits Stock Tokens, receives an ERC-20 basket share, sees that share priced from Chainlink feeds, and can redeem back into the underlying components.

A user interacts with a frontend that approves Stock Tokens and calls the basket contract, while the factory deploys baskets and Chainlink feeds provide pricing.

You can run it locally, deploy it to Robinhood Chain testnet, fork-test the mainnet configuration, and inspect the live walkthrough at robinhood-chain-dapp.vercel.app with the source at github.com/hummusonrails/robinhood-chain-dapp-example.

The chain is custom, your app path is not

Robinhood Chain is a custom Arbitrum Chain built on the Arbitrum stack. It uses Ethereum blobs for data availability and ETH as the gas token.

For you, that means the first deploy looks like an EVM deploy:

export PRIVATE_KEY=0x<your_private_key>
export RH_RPC_URL=https://rpc.testnet.chain.robinhood.com

forge create src/MyContract.sol:MyContract \
  --rpc-url $RH_RPC_URL \
  --private-key $PRIVATE_KEY \
  --broadcast

This is where the Arbitrum stack matters. Robinhood gets a dedicated chain with custom block production, pricing, infrastructure, and product controls. Builders still get the Ethereum contract model and tooling.

Arbitrum as a platform: Arbitrum One is the credibly neutral public chain with deep shared liquidity, while Arbitrum Chains give teams the flexible, feature-rich execution environment Robinhood Chain runs on.

That split is the whole reason the app path stays familiar while the chain is bespoke. Robinhood Chain runs on Arbitrum Nitro, the same technology as Arbitrum One, deployed as a dedicated chain. You inherit the parts you would not want to rebuild without doing anything:

What the Arbitrum stack buys you for free: Ethereum settlement with fraud proofs and a trustless canonical bridge, ETH gas with tiny fees, ERC-4337 and EIP-7702 account abstraction on day one, and Stylus for Rust contracts alongside Solidity.

State roots settle to Ethereum with fraud proofs, gas is paid in ETH, ERC-4337 and EIP-7702 account abstraction are live at canonical addresses, and Stylus lets a Solidity contract call a Rust one. None of that is work you take on to get a custom chain.

Stock Tokens are ERC-20s with one extra accounting layer

Raw ERC-20 balances remain stable for contracts, while the UI multiplier converts them into share-equivalent display amounts and can update after corporate actions.

Stock Tokens are ERC-20 tokens. You can read balances, transfer, approve, and compose them the same way you would any other token.

IERC20 nvda = IERC20(NVDA_TOKEN_ADDRESS);

uint256 balance = nvda.balanceOf(user);
nvda.approve(spender, amount);
nvda.transfer(recipient, amount);

The extra piece is corporate actions.

Stock Tokens implement ERC-8056, the Scaled UI Amount extension. Raw token balances do not rebase when a split or dividend adjustment changes the economic relationship between one token and the underlying equity. Instead, the token exposes a uiMultiplier().

interface IScaledUIAmount {
    function uiMultiplier() external view returns (uint256);
    function balanceOfUI(address account) external view returns (uint256);
    function totalSupplyUI() external view returns (uint256);
    function newUIMultiplier() external view returns (uint256);
    function effectiveAt() external view returns (uint256);
}

The display conversion is:

underlyingShares = rawBalance * uiMultiplier / 1e18;

That lets protocols keep using raw ERC-20 units while wallets and UIs show share-equivalent amounts. If a split changes the multiplier, your accounting does not suddenly mutate under your contract.

The basket app is small on purpose

The sample app you can start learning from has two contracts and no owner.

The interactive walkthrough prices one share live in the browser from the same Chainlink reads the contract uses: multiply each feed by the units backing a share, then sum.

The live math behind one TRIO share: 0.4 TSLA plus 0.3 AMZN plus 0.3 NFLX, each priced from its Chainlink feed, summing to the value returned by sharePriceUsd() onchain.

Contract Role Control Surface
BasketFactory Deploys and records baskets Permissionless
BasketToken Holds components, mints, redeems, prices shares No owner, no upgrade path

The factory does not custody anything. It deploys a basket and records the address.

function createBasket(
    string calldata name,
    string calldata symbol,
    BasketToken.Component[] calldata components,
    uint256 maxPriceAge
) external returns (address basket) {
    basket = address(new BasketToken(name, symbol, components, maxPriceAge));
    _baskets.push(basket);
    isBasket[basket] = true;
    emit BasketCreated(basket, msg.sender, name, symbol);
}

Each basket stores a fixed list of components. A component is a token, a feed, and the raw amount of that token backing one basket share.

struct Component {
    address token;
    address feed;
    uint256 unitsPerShare;
}

The mainnet demo basket uses TSLA, NVDA, and AAPL Stock Tokens with their Chainlink feeds:

function _mainnetComponents()
    internal
    pure
    returns (BasketToken.Component[] memory c)
{
    c = new BasketToken.Component[](3);
    c[0] = BasketToken.Component(MAINNET_TSLA, MAINNET_TSLA_FEED, 0.4e18);
    c[1] = BasketToken.Component(MAINNET_NVDA, MAINNET_NVDA_FEED, 0.3e18);
    c[2] = BasketToken.Component(MAINNET_AAPL, MAINNET_AAPL_FEED, 0.3e18);
}

One TRIO share is backed by 0.4 TSLA, 0.3 NVDA, and 0.3 AAPL.

On testnet, the demo uses real faucet Stock Tokens for TSLA, AMZN, and NFLX with mock Chainlink feeds. That is because the testnet gives you Stock Tokens to build with, while the production Stock Token feeds live on mainnet.

Minting is the ERC-20 path you already know

Minting starts with component approvals, then the basket pulls Stock Tokens and mints shares; redeeming burns shares and transfers components back.

From the frontend, minting is two steps: approve each component token, then call the basket.

await writeContract({
  address: stockTokenAddress,
  abi: erc20Abi,
  functionName: "approve",
  args: [basketAddress, amount],
});

await writeContract({
  address: basketAddress,
  abi: basketTokenAbi,
  functionName: "mint",
  args: [shares, account],
});

Onchain, the basket pulls the required component amounts and mints shares in the same transaction.

function mint(uint256 shares, address to) external nonReentrant {
    if (shares == 0) revert ZeroShares();

    uint256 count = _components.length;
    for (uint256 i = 0; i < count; i++) {
        Component memory c = _components[i];
        uint256 amount = Math.mulDiv(
            c.unitsPerShare,
            shares,
            SHARE_UNIT,
            Math.Rounding.Ceil
        );

        IERC20(c.token).safeTransferFrom(msg.sender, address(this), amount);
    }

    _mint(to, shares);
    emit Minted(msg.sender, to, shares);
}

The rounding direction matters. Mint rounds up so a minter cannot underpay the basket reserves by dust.

Redeem is the mirror image. Burn first, transfer components out, round down so reserves cannot be overdrawn.

function redeem(uint256 shares, address to) external nonReentrant {
    if (shares == 0) revert ZeroShares();
    if (to == address(0)) revert ZeroAddress();

    _burn(msg.sender, shares);

    uint256 count = _components.length;
    for (uint256 i = 0; i < count; i++) {
        Component memory c = _components[i];
        uint256 amount = Math.mulDiv(
            c.unitsPerShare,
            shares,
            SHARE_UNIT,
            Math.Rounding.Floor
        );

        IERC20(c.token).safeTransfer(to, amount);
    }

    emit Redeemed(msg.sender, to, shares);
}

Notice what redeem does not do. It does not read an oracle. It does not check a price. It does not ask the factory for permission.

That separation is deliberate. Pricing tells the UI what a share is worth. Redemption returns the tokens held by the contract.

The repo gives you the full deploy path

Start locally:

git clone --recurse-submodules https://github.com/hummusonrails/robinhood-chain-dapp-example.git
cd robinhood-chain-dapp-example

pnpm install
anvil
pnpm run deploy:local
pnpm run smoke
pnpm run dev

The local deploy creates mock Stock Tokens, mock Chainlink feeds, the factory, and a demo Tech Trio basket. It also writes apps/frontend/.env.local, so the frontend can read the deployed addresses.

For testnet, get ETH and Stock Tokens from the Robinhood Chain faucet, then run:

PRIVATE_KEY=$YOUR_TESTNET_KEY pnpm run deploy:testnet

One Foundry script handles local, testnet, and mainnet by switching on block.chainid.

function run() external {
    vm.startBroadcast();

    BasketFactory factory = new BasketFactory();
    console2.log("FACTORY=%s", address(factory));

    BasketToken.Component[] memory components;
    if (block.chainid == 4663) {
        components = _mainnetComponents();
    } else if (block.chainid == 46630) {
        components = _testnetComponents();
    } else {
        components = _localComponents();
    }

    address basket =
        factory.createBasket("Tech Trio", "TRIO", components, MAX_PRICE_AGE);

    console2.log("DEMO_BASKET=%s", basket);
    console2.log("CHAIN_ID=%s", block.chainid);

    vm.stopBroadcast();
}

The testnet deployment behind the live walkthrough is:

Contract Address
BasketFactory 0xC1940D5fd58ce735A44a53f910852B12250F6a14
BasketToken (TRIO) 0x7633e0920Ea46A8Ec54F61C95adECD391c01Edd4

For mainnet, the same script uses the real TSLA, NVDA, and AAPL Stock Tokens with their Chainlink feeds:

PRIVATE_KEY=$YOUR_MAINNET_KEY scripts/deploy.sh mainnet

Before spending mainnet gas, run the fork tests:

pnpm run test:contracts
pnpm run test:fork

The fork test creates the basket against live Robinhood Chain mainnet state, gives a test account Stock Token balances inside the fork, mints, checks backing, redeems, and verifies the supply returns to zero.

function test_mintAndRedeem_withRealStockTokens() public {
    deal(TSLA, alice, 1e18);
    deal(NVDA, alice, 1e18);
    deal(AAPL, alice, 1e18);

    vm.startPrank(alice);
    IERC20Metadata(TSLA).approve(address(basket), type(uint256).max);
    IERC20Metadata(NVDA).approve(address(basket), type(uint256).max);
    IERC20Metadata(AAPL).approve(address(basket), type(uint256).max);

    basket.mint(2e18, alice);
    assertEq(basket.balanceOf(alice), 2e18);
    assertEq(IERC20Metadata(TSLA).balanceOf(address(basket)), 0.8e18);

    basket.redeem(2e18, alice);
    assertEq(basket.totalSupply(), 0);
    assertEq(IERC20Metadata(TSLA).balanceOf(alice), 1e18);
    vm.stopPrank();
}

That is the development loop you want for this kind of app: mocks for speed, testnet for wallet flow, fork tests for live integration assumptions, mainnet only after the path is boring.

The testnet deploy behind the live walkthrough is a real session. Five contracts, priced in ETH, verified on Blockscout, for a fraction of a cent of gas.

A recorded testnet deployment on Robinhood Chain: an estimated 0.000122 ETH for the whole five-contract script, with all contracts verified on Blockscout and addresses handed to the frontend.

That price comes from how the Arbitrum stack orders and settles work. Transactions arrive first come, first served into roughly 250ms blocks, get batched, and post to Ethereum as blob data.

The life of a transaction on Robinhood Chain: your transaction hits the first-come-first-served sequencer, lands in a ~250ms block, is compressed into a batch, and settles to Ethereum for blob data availability. The fee is L2 execution gas plus the L1 data fee.

What changes when the ERC-20 tracks a market asset

A Stock Token still fits the ERC-20 interface, but the surrounding assumptions change.

Concern Generic ERC-20 Habit Stock Token Pattern
User balance Show balanceOf Show balanceOfUI or apply uiMultiplier for share-equivalent display
Price Maybe no canonical feed Read the per-token Chainlink feed on mainnet
Corporate actions Often ignored Track multiplier updates and pending effective times
Valuation Token price only Feed price already includes the multiplier
Staleness Crypto feeds may update continuously Stock feeds follow market hours
Exit path Often price-gated Prefer collateral redemption that does not depend on oracle reads

“... Robinhood Chain lays the groundwork for an ecosystem that will define the future of tokenized real-world assets and enable builders to tap into DeFi liquidity within the Ethereum ecosystem.”

Johann Kerbrat, SVP and GM of Crypto and International at Robinhood

Read that as an engineering prompt, not a slogan. If equities, ETFs, feeds, and lending markets become normal onchain components, then app design gets more interesting than “deploy another token.” You can build products where ownership, pricing, collateral, and settlement are all programmable.

From an app to your own Arbitrum chain

The basket is one app on Robinhood Chain. Robinhood Chain is one chain on Arbitrum. That nesting is the point: the same stack meets you wherever you are on the path.

You can ship an app on Arbitrum One first and tap into deep shared liquidity, lean on the team for deployment and go-to-market, and compose with the 1,000+ protocols already deployed. When your product needs its own blockspace, pricing, and controls, the same stack graduates you to a custom Arbitrum Chain, exactly the move Robinhood made.

Arbitrum as a launch partner: start with a deployment on Arbitrum One, lean on a trusted team and deep liquidity across 1,000+ deployed protocols, and graduate to your own Arbitrum Chain when your product needs it.

The economics travel with you. Because Robinhood Chain settles to Ethereum as blob data on the Arbitrum stack, the cost to transact keeps trending down, recently around a tenth of a cent per transaction.

Token Terminal data on the median transaction fee for Robinhood Chain, around $0.00127 per transaction and trending down, powered by Arbitrum and Ethereum.

Whether you stay as an app or become a chain, the contract model, the tooling, and the settlement guarantees are the same ones you used on day one.

Start with the working system

The index basket is not the only app to build on Robinhood Chain. It is a useful first one to explore because it touches the surfaces your own app will probably need:

  • Stock Token reads, approvals, and transfers
  • ERC-8056 display logic
  • Chainlink Stock Token feeds
  • Mint and redeem flows
  • Local mocks
  • Robinhood Chain testnet deployment
  • Blockscout verification
  • Mainnet fork tests
  • A Next.js frontend using wagmi and viem

The useful pattern is not “copy this basket forever.” It is the shape of the work: keep contracts small, separate pricing from settlement, treat real-world asset details as part of the protocol surface, and test the same assumptions from local mocks to live mainnet state.

Explore the fully functional index basket app deployed on Robinhood Chain with its interactive developer walkthrough and open source code on GitHub: robinhood-chain-dapp.vercel.app and github.com/hummusonrails/robinhood-chain-dapp-example.

Read more