A complete implementation of native RBTC Vault smart contracts written in Yul and Huff, deployable on Rootstock, built with Foundry. No Solidity for contract logic—only low-level EVM assembly.
This tutorial demonstrates how to write, compile, and deploy Yul and Huff smart contracts on Rootstock. Rootstock is fully EVM-compatible, so standard EVM bytecode runs without modification. While most documentation focuses on Solidity, this project shows that Rootstock executes raw EVM bytecode from low-level languages, enabling gas-optimized and size-critical contracts on Bitcoin-secured infrastructure.
- ✅ No Solidity Logic: Contract behavior is implemented entirely in Yul (Solidity’s assembly) and Huff (macro-based assembly)
- ✅ Gas & Size Control: Full control over opcodes and bytecode layout for optimization
- ✅ Same Interface: Both implementations expose the same
IVaultinterface (deposit, withdraw, balanceOf) - ✅ Rootstock Native: Standard EVM bytecode; deploy once to Rootstock Testnet or Mainnet
- ✅ Tested with Foundry: Solidity used only for tests and deployment scripts
┌─────────────────┐ ┌─────────────────┐
│ Vault.yul │ │ Vault.huff │
│ (strict asm) │ │ (macros+dispatch)│
└────────┬────────┘ └────────┬────────┘
│ │
│ solc --strict-assembly│ huffc --bin-runtime
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ vault_yul.bin │ │ vault_huff.bin │
│ (creation code) │ │ (creation code) │
└────────┬────────┘ └────────┬────────┘
│ │
└────────────┬───────────┘
│
│ forge script (--broadcast)
▼
┌───────────────┐
│ Rootstock │
│ (Chain ID 31)│
│ Testnet / │
│ Mainnet │
└───────────────┘
RootVault_Lab/
├── src/
│ ├── yul/
│ │ └── Vault.yul # Yul Vault (strict assembly)
│ ├── huff/
│ │ └── Vault.huff # Huff Vault (macros + manual selector dispatch)
│ └── IVault.sol # Shared interface for tests and integration
├── script/
│ ├── compile.sh # Builds out/vault_yul.bin, out/vault_huff.bin
│ ├── DeployYulVault.s.sol # Deploy Yul Vault to Rootstock
│ └── DeployHuffVault.s.sol # Deploy Huff Vault to Rootstock
├── test/
│ ├── Deployer.sol # Deploys raw bytecode (used by tests)
│ ├── YulVault.t.sol # Full test suite for Yul Vault
│ └── HuffVault.t.sol # Gas tests for Huff Vault
├── out/ # Compiled bytecode (generated by compile.sh)
├── foundry.toml # Foundry config + Rootstock RPC endpoint
└── .env.example # Environment template for deployment
- Foundry installed
- solc (Solidity compiler) for compiling Yul
- huffc (Huff compiler) for compiling Huff—see Installing huffc below
- A Rootstock testnet RPC endpoint (for deployment)
- Clone the repository:
git clone https://github.com/<your-username>/RootVault_Lab.git
cd RootVault_Lab- Install Forge dependencies (if any):
forge build- Compile Yul and Huff to bytecode (required before tests or deploy):
chmod +x script/compile.sh
./script/compile.shThis produces out/vault_yul.bin, out/vault_yul.hex, out/vault_huff.bin, and out/vault_huff.hex. Yul compilation requires solc on your PATH; Huff requires huffc.
You need huffc only to compile the Huff Vault and run Huff tests.
Option A — Official installer
curl -L get.huff.sh | bash
source ~/.bashrc # or source ~/.zshrc
huffup
huffc --help # verifyOption B — From source (Rust required)
cargo install --git https://github.com/huff-language/huff-rs.git huff_cli --bins --lockedAfter running ./script/compile.sh, test both vaults locally:
forge testFor verbose output:
forge test -vvWith gas report:
forge test --gas-reportSet up environment variables:
cp .env.example .env
# Edit .env: set ROOTSTOCK_RPC_URL and PRIVATE_KEYGet testnet RBTC from the Rootstock Faucet.
Deploy Yul Vault:
source .env
forge script script/DeployYulVault.s.sol:DeployYulVaultScript \
--rpc-url $ROOTSTOCK_RPC_URL \
--private-key $PRIVATE_KEY \
--broadcast \
--chain-id 31 \
--legacyDeploy Huff Vault (after ./script/compile.sh with huffc installed):
source .env
forge script script/DeployHuffVault.s.sol:DeployHuffVaultScript \
--rpc-url $ROOTSTOCK_RPC_URL \
--private-key $PRIVATE_KEY \
--broadcast \
--chain-id 31 \
--legacyNote: Use --legacy because Rootstock testnet does not support EIP-1559 transaction type.
If ROOTSTOCK_RPC_URL is set in .env, you can use the configured alias:
source .env
forge script script/DeployYulVault.s.sol:DeployYulVaultScript \
--rpc-url rootstock_testnet \
--private-key $PRIVATE_KEY \
--broadcast \
--chain-id 31 \
--legacyShared interface implemented by both Yul and Huff vaults:
deposit(): Payable; creditsmsg.valuetomsg.sender’s balancewithdraw(uint256 amount): Deductsamountfrom caller’s balance and sends RBTC to callerbalanceOf(address user): View; returns the balance ofuser
Pure Yul (strict assembly) with manual selector dispatch:
- Storage: Balances at slot
keccak256(address, 0)(mapping at slot 0) - Selectors:
deposit()=0xd0e30db0,withdraw(uint256)=0x2e1a7d4d,balanceOf(address)=0x70a08231 - Checks: Revert on zero deposit, insufficient balance, or failed transfer (checks–effects–interactions)
Key flow: Load 4-byte selector from calldata → switch to the correct block → execute logic → stop() or return.
Huff macros and manual dispatcher at the end of MAIN:
- Storage: Same layout as Yul—
keccak256(abi.encode(key, 0))for balances (usessha3opcode in Huff) - Macros:
DEPOSIT(),WITHDRAW(),BALANCE_OF(),BALANCE_SLOT_FOR()for slot computation - Checks: Same revert conditions as Yul (zero deposit, insufficient balance, failed
call)
Key flow: Entry jumps to dispatcher → compare selector → jump to the correct macro block.
Compile the Yul source to bytecode:
solc --strict-assembly --bin src/yul/Vault.yulThe script compile.sh writes the output to out/vault_yul.hex and out/vault_yul.bin.
Compile the Huff source to runtime bytecode, then wrap it in minimal creation code so deployed code matches the runtime (correct jump targets):
huffc src/huff/Vault.huff --bin-runtimecompile.sh builds the creation wrapper and writes out/vault_huff.bin.
Tests deploy the bytecode via a small Deployer contract and call the vault through IVault:
./script/compile.sh # ensure out/ is up to date
forge test -vvEnsure .env has ROOTSTOCK_RPC_URL and PRIVATE_KEY. Then:
source .env
forge script script/DeployYulVault.s.sol:DeployYulVaultScript \
--rpc-url $ROOTSTOCK_RPC_URL \
--private-key $PRIVATE_KEY \
--broadcast \
--chain-id 31 \
--legacySave the deployed contract address from the output.
Same flow for the Huff bytecode:
forge script script/DeployHuffVault.s.sol:DeployHuffVaultScript \
--rpc-url $ROOTSTOCK_RPC_URL \
--private-key $PRIVATE_KEY \
--broadcast \
--chain-id 31 \
--legacyUse Foundry’s cast to call the vault (replace <VAULT_ADDRESS> and <USER_ADDRESS>):
# Check balance
cast call <VAULT_ADDRESS> "balanceOf(address)(uint256)" <USER_ADDRESS> --rpc-url $ROOTSTOCK_RPC_URL
# Deposit (send RBTC with the call)
cast send <VAULT_ADDRESS> "deposit()" --value 1ether --private-key $PRIVATE_KEY --rpc-url $ROOTSTOCK_RPC_URL
# Withdraw
cast send <VAULT_ADDRESS> "withdraw(uint256)" 1000000000000000000 --private-key $PRIVATE_KEY --rpc-url $ROOTSTOCK_RPC_URLBoth implementations use the same layout:
- Slot 0: Base slot for the balances mapping
- Balance of
user:keccak256(abi.encode(user, 0))— same as Solidity’smapping(address => uint256)at slot 0
No inheritance or proxy; storage is straightforward and identical in Yul and Huff.
Rootstock executes standard EVM bytecode. There is no special “Rootstock bytecode” format. Contracts use only common opcodes (caller, callvalue, sstore, sload, keccak256, call, revert, etc.), so the same artifacts run on Ethereum, Rootstock Testnet, and Rootstock Mainnet. Use --chain-id 31 for testnet and --legacy because Rootstock testnet does not support EIP-1559.
The test suite covers:
- ✅ Deposit increases balance (Yul + Huff)
- ✅ Withdraw decreases balance and sends RBTC (Yul)
- ✅ Revert on zero deposit (Yul)
- ✅ Revert on insufficient balance (Yul)
- ✅
balanceOffor unrelated address returns zero (Yul) - ✅ Gas logging for deposit and withdraw (Yul + Huff)
Run tests:
# All tests (requires ./script/compile.sh first)
forge test
# Verbose
forge test -vv
# Gas report
forge test --gas-report
# Only Yul tests
forge test --match-path test/YulVault.t.sol
# Only Huff tests
forge test --match-path test/HuffVault.t.solView deployed contracts on the Rootstock Testnet Explorer.
source .env
forge script script/DeployYulVault.s.sol:DeployYulVaultScript \
--rpc-url $ROOTSTOCK_RPC_URL \
--private-key $PRIVATE_KEY \
--broadcast \
--chain-id 31 \
--legacy# Use mainnet RPC (e.g. https://public-node.rsk.co)
forge script script/DeployYulVault.s.sol:DeployYulVaultScript \
--rpc-url $ROOTSTOCK_MAINNET_RPC \
--private-key $PRIVATE_KEY \
--broadcast \
--chain-id 30 \
--legacyCreate a .env file (use .env.example as template):
# Rootstock Testnet (Chain ID 31)
ROOTSTOCK_RPC_URL=https://public-node.testnet.rsk.co
# Deployer private key (no 0x prefix is fine)
PRIVATE_KEY=your_private_key_hereGet testnet RBTC from the Rootstock Faucet.
- Checks–Effects–Interactions: Both contracts update storage before external
call(withdraw). - Input Validation: Revert on zero deposit and on withdraw when balance is insufficient.
- Transfer Failure: Revert if the RBTC transfer in
withdrawfails. - No Upgradeability: Contracts are not proxy-based; bytecode is immutable after deployment.
- Private Key: Never commit
.env; keepPRIVATE_KEYsecure.
- Run
./script/compile.shso thatout/vault_yul.binand (if using Huff)out/vault_huff.binexist. - Ensure
solcis onPATHfor Yul; ensurehuffcis installed for Huff.
- Install Solidity and add it to your
PATH, or use the correctsolcversion for your platform.
- Install huffc using one of the options in Installing huffc. Run
huffc --helpto verify.
- Ensure the deployer account has enough RBTC on the target network (use the faucet on testnet).
- Use
--legacyfor Rootstock testnet (EIP-1559 not supported).
- Confirm
ROOTSTOCK_RPC_URLandPRIVATE_KEYin.envare correct. - For testnet, use
--chain-id 31and--legacy.
- Foundry Book
- Solidity — Yul
- Huff Documentation
- Rootstock Documentation
- Rootstock Testnet Explorer
- Rootstock Faucet
MIT
Contributions are welcome! Please open an issue or submit a pull request.
Built for Rootstock using Foundry, with Yul and Huff for low-level EVM development.
Happy Building on Rootstock! 🚀