I built a small index basket app on Robinhood Chain because I wanted to understand the developer path from the first contract deploy all the way to a working frontend.
The app is intentionally plain: a user deposits Stock Tokens, which are blockchain tokens that represent real equity exposure, and receives an ERC-20 basket share. ERC-20 is Ethereum's standard token interface, so a compatible token exposes familiar methods like balanceOf, transfer, and approve. The basket share is priced from live price feeds, and the user can redeem it back into the underlying Stock Tokens.
That's the part that made this interesting to me. The chain is custom, but the app path is not. I still wrote Solidity, deployed with Foundry, read contract state with viem, and wrote transactions from React with wagmi.
If you've built normal web apps, think of the chain's RPC endpoint as the API base URL. A wallet is login plus a signing key. A smart contract is backend code you deploy to the chain, except you should treat it like immutable infrastructure because you don't get to hot-patch it casually later.
The demo and source are here:
- App: https://robinhood-chain-dapp.vercel.app/
- Code: https://github.com/hummusonrails/robinhood-chain-dapp-example
The custom chain still feels like the EVM
Robinhood Chain is a custom Arbitrum Chain, which means it runs as a dedicated chain on the stack of Arbitrum, an Ethereum scaling system. It is also EVM-compatible. EVM means Ethereum Virtual Machine, the runtime that executes Solidity contracts, so the tooling surface looks like the Ethereum developer flow many tutorials already teach.
An L2, or rollup, is a chain that executes transactions separately and then posts compressed proof or transaction data back to Ethereum. Robinhood Chain uses Ethereum blobs for data availability, which is a cheaper Ethereum data lane for rollups to publish the data needed to reconstruct chain state. Gas, the metered compute fee you pay to run transactions, is paid in ETH.
The first deploy looks like this:
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
Foundry is the contract build, test, and deploy CLI. forge create compiles the contract, sends the deployment transaction, and broadcasts it to the RPC endpoint.
This is the part I appreciate as a developer. Robinhood gets its own chain configuration, infrastructure, pricing, and product controls. I still get the contract model I know how to reason about.
Stock Tokens are ERC-20s with display accounting
Stock Tokens are ERC-20s that represent real market assets. That means contracts can read balances, request approvals, and transfer them using the same functions they would use for any other ERC-20.
IERC20 nvda = IERC20(NVDA_TOKEN_ADDRESS);
uint256 balance = nvda.balanceOf(user);
nvda.approve(spender, amount);
nvda.transfer(recipient, amount);
The wrinkle is corporate actions. Stocks split. Dividends happen. The economic relationship between one token and one underlying share can change.
Stock Tokens implement ERC-8056, the Scaled UI Amount extension. Raw token balances stay stable for contracts. A UI multiplier is how wallets and apps display the share-equivalent amount.
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;
Why not just mutate balances after a split?
Because contracts depend on stable accounting. If my basket contract holds a raw token balance, I don't want a display-level corporate action to unexpectedly rewrite the reserve math inside the contract. The UI can show share-equivalent amounts, and the protocol can keep using raw ERC-20 units.
The basket contract does only a few things
The sample app has two contracts and no owner.
| Contract | Role | Control surface |
|---|---|---|
BasketFactory |
Deploys and records baskets | Permissionless |
BasketToken |
Holds components, mints, redeems, prices shares | No owner or upgrade path |
The factory does not custody user funds. 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 price feed, and the amount of that token backing one basket share.
struct Component {
address token;
address feed;
uint256 unitsPerShare;
}
A price feed is an external data source a contract or app can read. In this app, the feeds come from Chainlink, an oracle network that publishes market data onchain. An oracle is the bridge between offchain facts, like a stock price, and onchain code.
On mainnet, the production chain where real assets move, the 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, which is a staging chain with no real funds at risk, the demo uses faucet Stock Tokens for TSLA, AMZN, and NFLX with mock feeds. A faucet is a service that gives you test tokens so you can build without spending real money.
Minting follows the ERC-20 approval pattern
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],
});
That writeContract call is from wagmi, a React library for wallet connections and contract writes. viem is the TypeScript Ethereum client underneath it for typed reads, writes, and transaction handling.
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 user cannot underpay the basket reserves by tiny decimal leftovers.
Redeem is the mirror image. Burn first, transfer components out, and round down so the 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);
}
Redeem skips the price feed and the factory. It burns shares and returns the component tokens the contract already holds.
I keep pricing and redemption apart on purpose. You use the price for the UI. Redeem returns the collateral.
Local to mainnet is the path I want rehearsed
The repo gives you the whole loop.
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
anvil is Foundry's local development chain. Think of it as a throwaway local server for contracts. 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 knows which addresses to call.
For Robinhood Chain testnet:
PRIVATE_KEY=$YOUR_TESTNET_KEY pnpm run deploy:testnet
One Foundry script handles local, testnet, and mainnet by checking the chainid, which is the chain's network identifier.
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 |
Before spending mainnet gas, I want fork tests. A fork test runs tests against a local copy of live chain state, so you can check integration assumptions without sending real transactions.
pnpm run test:contracts
pnpm run test:fork
Here is the shape of the fork test:
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 I want for this kind of app: mocks for speed, testnet for wallet flow, fork tests for live integration assumptions, and mainnet only after the path is rehearsed.
A block explorer, which is basically hosted request logs for a chain, then gives you a way to inspect deployed contracts and transactions. The testnet contracts are verified on Blockscout, so you can read the source and calls after deployment.
Stock Tokens change the app assumptions
Stock Tokens still fit the ERC-20 interface, but the surrounding assumptions are different from a generic token.
For user balances, don't blindly show balanceOf. Use balanceOfUI or apply uiMultiplier so the user sees the share-equivalent amount.
For prices, read the per-token Chainlink feed on mainnet. For corporate actions, track multiplier updates and pending effective times. For valuation, remember that the feed price already includes the multiplier.
Stock market hours matter too. Crypto feeds may update around the clock. Stock feeds follow market sessions, so stale data checks need to reflect that.
The exit path is the one I care about most. If a user wants to redeem their basket share, I don't want that flow blocked because an oracle read is stale. The contract already holds the component tokens. Redemption should return the collateral.
Learn the chain later; start with the app
Robinhood Chain runs on Arbitrum Nitro, the same underlying technology as Arbitrum One, deployed as a dedicated chain. Arbitrum One is the public shared L2. A custom Arbitrum Chain gives a team its own execution environment while keeping the Ethereum-style contract model.
The mechanics under the hood are also why the fees are small. Transactions hit a sequencer, which is the service that orders transactions for the rollup, land in fast blocks, get batched, and settle back to Ethereum using blob data. The fee combines L2 execution gas with the data cost on L1, which is Ethereum itself.
That's useful context, but I wouldn't start by trying to absorb the whole chain architecture.
Start with the working system. Read a Stock Token balance. Approve a token. Mint a share. Redeem it. Check the price feed. Run the fork test. Look at the transaction in a block explorer.
Plenty of apps can live on Robinhood Chain. This basket is a good first build because it touches the surfaces most apps using market assets will need: token reads, approvals, ERC-8056 display logic, Chainlink feeds, local mocks, testnet deployment, verified contracts, fork tests, and a Next.js frontend using wagmi and viem.
What I learned from building it is where the real work sits: deciding where accounting belongs, where pricing belongs, and which assumptions deserve a test before real users and real assets touch the contract. The app stack itself is familiar.


