US stocks, as one on-chain basket.

Orbix vaults hold a basket of tokenized stocks. You deposit the basket’s tokens and get one vault token that represents your share of everything inside.

ORBX vault
CAEuaBDQ7emUqAogDVX3Qkom9mAtWtYTbwg637H9zhpump

Vaults

Each vault’s numbers are read straight from Solana.

How a vault works

Orbix does not issue stocks. It holds tokenized stocks from licensed issuers.

  1. Deposit the basket

    Send each token in the basket from your wallet, in the right proportions.

  2. Get vault tokens

    The program mints vault tokens equal to your slice of the vault.

  3. Hold it

    Vault tokens sit in your wallet. The reserves behind them are public on-chain.

  4. Redeem any time

    Burn vault tokens to take your share of every token back. A fee of up to 1% applies.

What you get from Orbix

The benefits, in plain terms.

One token, a whole basket

Hold several tokenized stocks through a single vault token, instead of buying and tracking each one yourself.

Reserves you can check

Every token a vault holds is visible on Solana. You don’t have to take our word for it.

Redeem on your own

Burn your vault tokens and take your share of every token back. No request form, no approval from us.

No withdrawal switch

The vault program has no function that lets anyone take user deposits out. The source code is public on this page.

A fee with a hard cap

The redeem fee is capped at 1% in the program itself, and each vault shows its exact fee.

Phone or computer

Connect Phantom, Solflare or Backpack on desktop, or open the site inside your wallet app on mobile.

Orbix does not promise returns. Stock prices go up and down, and so will the value of a vault token.

How Orbix earns

You should know how the project makes money before you use it.

One fee, set in the code

When someone redeems, up to 1% of the vault tokens they burn are minted to the Orbix treasury. The fee is fixed when a vault is created and shown on its page. It can’t go above 1%.

What that means for you

  • Orbix earns when vaults are used, not from promises of returns.
  • The treasury is paid in vault tokens, so it holds the same basket you do.
  • Orbix does not lend out deposits or charge hidden fees.

Reserves, in public

What each vault holds right now, read from the chain.

VaultToken (mint)Vault balanceBacking per 1 vault token
Reserves appear here once a vault is live.

Verify the program

The vault program’s source, so anyone can check how deposits and redemptions work.

lib.rs
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Burn, Mint, MintTo, Token, TokenAccount, Transfer};

// Replace after `anchor keys sync`.
declare_id!("11111111111111111111111111111111");

pub const MAX_COMPONENTS: usize = 5;
pub const ONE_SHARE: u128 = 1_000_000; // share mint has 6 decimals
pub const MAX_FEE_BPS: u16 = 100; // 1% hard cap on redeem fee

#[program]
pub mod orbix_vault {
    use super::*;

    /// Create a vault for a fixed basket of up to 5 SPL tokens.
    /// `units[i]` = raw amount of component i that backs 1 whole share at launch.
    pub fn initialize_vault(
        ctx: Context<InitializeVault>,
        vault_id: u8,
        fee_bps: u16,
        mints: Vec<Pubkey>,
        units: Vec<u64>,
    ) -> Result<()> {
        require!(fee_bps <= MAX_FEE_BPS, VaultError::FeeTooHigh);
        require!(
            !mints.is_empty() && mints.len() <= MAX_COMPONENTS && mints.len() == units.len(),
            VaultError::BadBasket
        );
        require!(units.iter().all(|u| *u > 0), VaultError::BadBasket);
        for i in 0..mints.len() {
            for j in (i + 1)..mints.len() {
                require!(mints[i] != mints[j], VaultError::BadBasket);
            }
        }
        let v = &mut ctx.accounts.vault;
        v.authority = ctx.accounts.authority.key();
        v.treasury = ctx.accounts.treasury.key();
        v.share_mint = ctx.accounts.share_mint.key();
        v.vault_id = vault_id;
        v.bump = ctx.bumps.vault;
        v.fee_bps = fee_bps;
        v.n = mints.len() as u8;
        for (i, m) in mints.iter().enumerate() {
            v.mints[i] = *m;
            v.units[i] = units[i];
        }
        Ok(())
    }

    /// Deposit the basket proportionally and receive `shares` raw share units.
    /// remaining_accounts: for each component i -> [user_token_i, vault_token_i]
    pub fn deposit<'info>(
        ctx: Context<'_, '_, 'info, 'info, Deposit<'info>>,
        shares: u64,
    ) -> Result<()> {
        require!(shares > 0, VaultError::ZeroAmount);
        let vault = &ctx.accounts.vault;
        let n = vault.n as usize;
        require!(ctx.remaining_accounts.len() == n * 2, VaultError::BadAccounts);
        let supply = ctx.accounts.share_mint.supply as u128;

        for i in 0..n {
            let user_ta = Account::<TokenAccount>::try_from(&ctx.remaining_accounts[i * 2])?;
            let vault_ta = Account::<TokenAccount>::try_from(&ctx.remaining_accounts[i * 2 + 1])?;
            require_keys_eq!(user_ta.owner, ctx.accounts.user.key(), VaultError::BadAccounts);
            require_keys_eq!(user_ta.mint, vault.mints[i], VaultError::BadAccounts);
            require_keys_eq!(vault_ta.mint, vault.mints[i], VaultError::BadAccounts);
            require_keys_eq!(vault_ta.owner, vault.key(), VaultError::BadAccounts);

            let need: u128 = if supply == 0 {
                ceil_div((shares as u128) * (vault.units[i] as u128), ONE_SHARE)
            } else {
                require!(vault_ta.amount > 0, VaultError::EmptyReserve);
                ceil_div((shares as u128) * (vault_ta.amount as u128), supply)
            };
            require!(need > 0 && need <= u64::MAX as u128, VaultError::MathError);
            token::transfer(
                CpiContext::new(
                    ctx.accounts.token_program.to_account_info(),
                    Transfer {
                        from: user_ta.to_account_info(),
                        to: vault_ta.to_account_info(),
                        authority: ctx.accounts.user.to_account_info(),
                    },
                ),
                need as u64,
            )?;
        }

        let id = [vault.vault_id];
        let seeds: &[&[u8]] = &[b"vault", vault.authority.as_ref(), &id, &[vault.bump]];
        token::mint_to(
            CpiContext::new_with_signer(
                ctx.accounts.token_program.to_account_info(),
                MintTo {
                    mint: ctx.accounts.share_mint.to_account_info(),
                    to: ctx.accounts.user_shares.to_account_info(),
                    authority: ctx.accounts.vault.to_account_info(),
                },
                &[seeds],
            ),
            shares,
        )?;
        Ok(())
    }

    /// Burn `shares` and receive the proportional basket. A small fee (in shares)
    /// is minted to the treasury.
    /// remaining_accounts: for each component i -> [user_token_i, vault_token_i]
    pub fn redeem<'info>(
        ctx: Context<'_, '_, 'info, 'info, Redeem<'info>>,
        shares: u64,
    ) -> Result<()> {
        require!(shares > 0, VaultError::ZeroAmount);
        let vault = &ctx.accounts.vault;
        let n = vault.n as usize;
        require!(ctx.remaining_accounts.len() == n * 2, VaultError::BadAccounts);
        let supply = ctx.accounts.share_mint.supply as u128;
        require!(supply >= shares as u128, VaultError::MathError);

        let fee = ((shares as u128) * (vault.fee_bps as u128) / 10_000) as u64;
        let net = shares - fee;

        // Burn the user's shares first.
        token::burn(
            CpiContext::new(
                ctx.accounts.token_program.to_account_info(),
                Burn {
                    mint: ctx.accounts.share_mint.to_account_info(),
                    from: ctx.accounts.user_shares.to_account_info(),
                    authority: ctx.accounts.user.to_account_info(),
                },
            ),
            shares,
        )?;

        let id = [vault.vault_id];
        let seeds: &[&[u8]] = &[b"vault", vault.authority.as_ref(), &id, &[vault.bump]];

        for i in 0..n {
            let user_ta = Account::<TokenAccount>::try_from(&ctx.remaining_accounts[i * 2])?;
            let vault_ta = Account::<TokenAccount>::try_from(&ctx.remaining_accounts[i * 2 + 1])?;
            require_keys_eq!(user_ta.owner, ctx.accounts.user.key(), VaultError::BadAccounts);
            require_keys_eq!(user_ta.mint, vault.mints[i], VaultError::BadAccounts);
            require_keys_eq!(vault_ta.mint, vault.mints[i], VaultError::BadAccounts);
            require_keys_eq!(vault_ta.owner, vault.key(), VaultError::BadAccounts);

            let out = (net as u128) * (vault_ta.amount as u128) / supply;
            if out == 0 {
                continue;
            }
            token::transfer(
                CpiContext::new_with_signer(
                    ctx.accounts.token_program.to_account_info(),
                    Transfer {
                        from: vault_ta.to_account_info(),
                        to: user_ta.to_account_info(),
                        authority: ctx.accounts.vault.to_account_info(),
                    },
                    &[seeds],
                ),
                out as u64,
            )?;
        }

        if fee > 0 {
            token::mint_to(
                CpiContext::new_with_signer(
                    ctx.accounts.token_program.to_account_info(),
                    MintTo {
                        mint: ctx.accounts.share_mint.to_account_info(),
                        to: ctx.accounts.treasury_shares.to_account_info(),
                        authority: ctx.accounts.vault.to_account_info(),
                    },
                    &[seeds],
                ),
                fee,
            )?;
        }
        Ok(())
    }
}

fn ceil_div(a: u128, b: u128) -> u128 {
    (a + b - 1) / b
}

#[account]
#[derive(InitSpace)]
pub struct Vault {
    pub authority: Pubkey,
    pub treasury: Pubkey,
    pub share_mint: Pubkey,
    pub vault_id: u8,
    pub bump: u8,
    pub fee_bps: u16,
    pub n: u8,
    pub mints: [Pubkey; MAX_COMPONENTS],
    pub units: [u64; MAX_COMPONENTS],
}

#[derive(Accounts)]
#[instruction(vault_id: u8)]
pub struct InitializeVault<'info> {
    #[account(mut)]
    pub authority: Signer<'info>,
    /// CHECK: wallet that receives fee shares; only its key is stored.
    pub treasury: UncheckedAccount<'info>,
    #[account(
        init, payer = authority, space = 8 + Vault::INIT_SPACE,
        seeds = [b"vault", authority.key().as_ref(), &[vault_id]], bump
    )]
    pub vault: Account<'info, Vault>,
    #[account(
        init, payer = authority, mint::decimals = 6, mint::authority = vault,
        seeds = [b"share", vault.key().as_ref()], bump
    )]
    pub share_mint: Account<'info, Mint>,
    pub token_program: Program<'info, Token>,
    pub system_program: Program<'info, System>,
    pub rent: Sysvar<'info, Rent>,
}

#[derive(Accounts)]
pub struct Deposit<'info> {
    #[account(mut)]
    pub user: Signer<'info>,
    #[account(
        seeds = [b"vault", vault.authority.as_ref(), &[vault.vault_id]], bump = vault.bump
    )]
    pub vault: Account<'info, Vault>,
    #[account(mut, address = vault.share_mint)]
    pub share_mint: Account<'info, Mint>,
    #[account(mut, token::mint = share_mint, token::authority = user)]
    pub user_shares: Account<'info, TokenAccount>,
    pub token_program: Program<'info, Token>,
}

#[derive(Accounts)]
pub struct Redeem<'info> {
    #[account(mut)]
    pub user: Signer<'info>,
    #[account(
        seeds = [b"vault", vault.authority.as_ref(), &[vault.vault_id]], bump = vault.bump
    )]
    pub vault: Account<'info, Vault>,
    #[account(mut, address = vault.share_mint)]
    pub share_mint: Account<'info, Mint>,
    #[account(mut, token::mint = share_mint, token::authority = user)]
    pub user_shares: Account<'info, TokenAccount>,
    #[account(mut, token::mint = share_mint, token::authority = vault.treasury)]
    pub treasury_shares: Account<'info, TokenAccount>,
    pub token_program: Program<'info, Token>,
}

#[error_code]
pub enum VaultError {
    #[msg("Fee above the 1% cap")]
    FeeTooHigh,
    #[msg("Invalid basket configuration")]
    BadBasket,
    #[msg("Amount must be above zero")]
    ZeroAmount,
    #[msg("Wrong or missing token accounts")]
    BadAccounts,
    #[msg("Vault reserve for a component is empty")]
    EmptyReserve,
    #[msg("Math error")]
    MathError,
}
Know the risks. Tokenized stocks can lose value and may be restricted in your country. Smart contracts can have bugs. Only deposit what you can afford to lose. Nothing on this site is investment advice.