Skip to content

3. Move USDC between spot and perp

On HyperCore, an account holds USDC in two balances: spot (8 decimals, for token trading) and perp (6 decimals, margin for perp positions). In this tutorial a contract moves USDC between them with core_writer.transfer_usd_class, and nothing leaves HyperCore. Tutorial 4 moves USDC between HyperEVM and HyperCore.

You'll learn:

  • How to move USDC from spot to perp and back from a contract.
  • Why transfer_usd_class takes 6-decimal perp USD, and why spot then changes by ntl * 100.
  • How to check the result through the spot balance, the perp account value and withdrawable.

Prerequisites

Item Amount
HYPE on HyperEVM less than 0.002 HYPE for gas
USDC in the spot balance of your Core account 6 USDC: 5 USDC to the contract and 1 USDC activation fee
HYPER_VYPER_TESTNET_KEY for all steps with --execute
HYPER_VYPER_CORE_FUNDER_KEY optional, for the fund step

Do tutorial 2 first, because it explains activation.

Units

The same USDC has two integer forms on Core:

Balance Decimals 3 USDC
Core spot USDC 8 300_000_000
Perp USD (transfer_usd_class, account value, withdrawable) 6 3_000_000

If you give 300_000_000 to transfer_usd_class, you ask for 300 USD, not 3.

The contract

# pragma version 0.4.3
# pragma evm-version cancun
# pragma optimize gas
# SPDX-License-Identifier: MIT

"""
@title Tutorial: move USDC between Core spot and perp
@notice The owner moves USDC between the spot balance and the perp balance of
        this contract on HyperCore. Spot USDC has 8 decimals. Perp USD has 6.
@dev Tutorial code. Nobody has audited it.
"""

from hyper_vyper import constants
from hyper_vyper import conversions
from hyper_vyper import core_writer
from hyper_vyper import precompiles


struct Status:
    evm_block: uint256
    core_exists: bool
    spot_usdc: uint64  # Core USDC, 8 decimals
    spot_usdc_hold: uint64  # Core USDC, 8 decimals
    spot_hype: uint64  # Core HYPE, 8 decimals
    perp_account_value: int64  # perp USD, 6 decimals
    withdrawable: uint64  # perp USD, 6 decimals


OWNER: public(immutable(address))


@deploy
def __init__():
    OWNER = msg.sender


@internal
@view
def _only_owner():
    assert msg.sender == OWNER, "Only the owner"


@external
@view
def status() -> Status:
    usdc: precompiles.SpotBalance = precompiles.spot_balance(self, constants.USDC_TOKEN_INDEX)
    perp: precompiles.AccountMarginSummary = precompiles.account_margin_summary(
        constants.DEFAULT_PERP_DEX, self
    )
    return Status(
        evm_block=block.number,
        core_exists=precompiles.core_user_exists(self),
        spot_usdc=usdc.total,
        spot_usdc_hold=usdc.hold,
        spot_hype=precompiles.spot_balance(self, conversions.hype_token_index()).total,
        perp_account_value=perp.account_value,
        withdrawable=precompiles.withdrawable(self),
    )


@external
def move_usdc(ntl: uint64, to_perp: bool):
    """Move ntl perp USD (6 decimals) from spot to perp (to_perp) or back.
    Core debits ntl * 100 Core wei (8 decimals) from spot for a move to perp.
    A move that Core rejects fails silently. The EVM transaction succeeds."""
    self._only_owner()
    assert precompiles.core_user_exists(self), "Core account not activated"
    assert ntl > 0, "Zero amount"
    core_writer.transfer_usd_class(ntl, to_perp)


@external
def sweep(token: uint64, amount_wei: uint64):
    """Send Core spot funds of this contract to the owner on Core (cleanup).
    amount_wei is in Core wei: 8 decimals for USDC and for HYPE."""
    self._only_owner()
    assert precompiles.core_user_exists(self), "Core account not activated"
    core_writer.send_asset(
        OWNER, empty(address), constants.SPOT_DEX, constants.SPOT_DEX, token, amount_wei
    )
  • move_usdc checks the owner, the activation and a non-zero amount before it sends the action.
  • status reads spot USDC, perp account value and withdrawable in one call.

Steps

  1. Deploy the contract:

    uv run --locked tutorials/spot_perp/run.py deploy --execute
    
  2. Fund the contract with 5 USDC on Core. This send also activates the account:

    uv run --locked tutorials/spot_perp/run.py fund --execute
    

    Without HYPER_VYPER_CORE_FUNDER_KEY, the step prints the send_core.py command. Run it, then check the account with the status step until core_user_exists is True.

    Core transfer: 5 USDC from your Core account to 0x...
      HYPER_VYPER_CORE_FUNDER_KEY=0x... uv run --script tutorials/send_core.py --to 0x... --token USDC --amount 5 --execute
      block N: exists True, spot USDC 500000000 (5 USDC, 8 decimals)
    
  3. Show the plan to move 3 USDC to perp. This step runs the call on a local copy of the chain and sends nothing:

    uv run --locked tutorials/spot_perp/run.py to-perp --amount 3
    
    before (EVM block N):
      spot USDC      500000000 (5 USDC, 8 decimals)
      perp value     0 (0 USD, 6 decimals)
      withdrawable   0 (0 USD, 6 decimals)
    move 3000000 (3 USD, 6 decimals) spot -> perp
      spot changes by 300000000 Core wei (8 decimals): ntl x 100
    plan: move_usdc(3000000, True) value 0
    plan only: add --execute to send. Nothing was sent.
    
  4. Move 3 USDC to perp:

    uv run --locked tutorials/spot_perp/run.py to-perp --amount 3 --execute
    
    plan: move_usdc(3000000, True) value 0
    sent move_usdc: 0x... in block N
      block N+57: spot USDC 2, perp 3
    after (EVM block N+57):
      spot USDC      200000000 (2 USDC, 8 decimals)
      perp value     3000000 (3 USD, 6 decimals)
      withdrawable   3000000 (3 USD, 6 decimals)
    change: spot USDC -300000000 (-3 USDC, 8 decimals)
            perp value 3000000 (3 USD, 6 decimals)
    

    Core applied the move in the next block. The script reads later, after it has the transaction receipt, and the public testnet RPC can take many blocks to return it.

  5. Move the 3 USDC back to spot:

    uv run --locked tutorials/spot_perp/run.py to-spot --amount 3 --execute
    

    The change is spot USDC 300000000 and perp value -3000000.

What can go wrong

  • No change after 20 blocks. Core rejected the action silently, even though the EVM transaction succeeded. The account may not be activated, spot may have too little USDC, or perp may have too little withdrawable. The script checks the balances before it sends, but the state can change in between.
  • A move out of perp fails while a position is open. A transfer out of perp must leave enough margin. The tutorial contract opens no positions.
  • Wrong units. move_usdc takes 6-decimal perp USD, so the script converts --amount 3 to 3000000.

Clean up

Move all USDC to spot (step 5), then send it back to your Core account:

uv run --locked tutorials/spot_perp/run.py sweep --execute
plan: sweep(0, 500000000) value 0
sent sweep: 0x... in block N
  block N+7: spot_usdc 0