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_classtakes 6-decimal perp USD, and why spot then changes byntl * 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_usdcchecks the owner, the activation and a non-zero amount before it sends the action.statusreads spot USDC, perp account value andwithdrawablein one call.
Steps
-
Deploy the contract:
-
Fund the contract with 5 USDC on Core. This send also activates the account:
Without
HYPER_VYPER_CORE_FUNDER_KEY, the step prints thesend_core.pycommand. Run it, then check the account with thestatusstep untilcore_user_existsisTrue. -
Show the plan to move 3 USDC to perp. This step runs the call on a local copy of the chain and sends nothing:
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. -
Move 3 USDC to perp:
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.
-
Move the 3 USDC back to spot:
The change is
spot USDC 300000000andperp 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_usdctakes 6-decimal perp USD, so the script converts--amount 3to3000000.
Clean up
Move all USDC to spot (step 5), then send it back to your Core account: