balance_floor

7cu · happy

Abort if an account's lamports balance is below the floor.

program idSLDwNtfXVRXuW29kMWLkvs8QX6xkdg8qjPuV6WQ25Hb

What it does

balance_floor answers: does this account hold at least N lamports? If yes, continue. If no, abort.

Useful as a rent-reserve check, or as a pre-condition that a keeper's operational balance is sufficient before triggering an action that might leave the account below the rent-exempt threshold.

Same shape as slippage but reads from the account's header (lamports) instead of its data block (token amount). 7 CU on the happy path.

How to use it

import { balanceFloorIx } from "@solana-asm/shield"import { PublicKey, Transaction } from "@solana/web3.js" const BALANCE_FLOOR = new PublicKey("SLDwNtfXVRXuW29kMWLkvs8QX6xkdg8qjPuV6WQ25Hb") const tx = new Transaction()tx.add(balanceFloorIx({  programId: BALANCE_FLOOR,  account: signer.publicKey,  minLamports: 1_000_000n,}))tx.add(yourDestinationInstruction)

Assembly walkthrough

lines 1-3

Constants for the byte offsets we will read

.equ INSTRUCTION_DATA_LEN, 0x2868.equ INSTRUCTION_DATA,     0x2870.equ ACCT0_LAMPORTS,       0x0050

Why INSTRUCTION_DATA_LEN = 0x2868 and INSTRUCTION_DATA = 0x2870? These are the standard offsets for any Shield guard that declares ONE account with no data. The per-account region for a zero-data account takes 0x2868 bytes (10,344 decimal). That includes the per-account header, alignment padding, and zero bytes of data. Whatever comes after that region is your ix metadata, so the length field lands at 0x2868 and the data starts 8 bytes later at 0x2870.

Compare to slippage's 0x2910/0x2918: slippage's account 0 is a real SPL Token account that holds 165 bytes of state, which pushes everything 168 bytes further along (165 rounded up to an 8-byte alignment boundary). Same fields, just shoved later because more sits in front of them. Compare to slot_deadline at 0x0008/0x0010: it declares zero accounts so nothing sits in front, and the offsets stay tiny.

Why ACCT0_LAMPORTS = 0x0050 (80 decimal)? The input region starts with an 8-byte u64 holding the account count. Right after that, the per-account header for account 0 is 8 more bytes (1-byte dup tag + 1-byte is_signer + 1-byte is_writable + 1-byte executable + 4 bytes of alignment padding), followed by the 32-byte pubkey and the 32-byte owner pubkey. Add them up: 8 + 8 + 32 + 32 = 80 bytes. The lamport balance is the next u64, so it sits at byte 80 (0x50). The runtime writes the live balance from chain state at instruction load time, so reading this offset gives us the up-to-date number.

lines 7-8

Check that the caller gave us exactly 8 bytes of ix data

  ldxdw r2, [r1 + INSTRUCTION_DATA_LEN]  jne r2, 8, bad_ix_data

Read the u64 ix data length the runtime wrote at INSTRUCTION_DATA_LEN, fail with exit 2 unless it equals 8.

Why exactly 8? The caller is supposed to hand us one u64 floor (minimum lamports), no more, no less. A 7-byte input leaves the top byte of the u64 undefined; a 9-byte input could sneak past a permissive SDK. Strict equality blocks both.

lines 10-13

Two loads and the actual safety check

  ldxdw r3, [r1 + ACCT0_LAMPORTS]  ldxdw r4, [r1 + INSTRUCTION_DATA]   jlt r3, r4, below_floor

ldxdw r3, [r1 + ACCT0_LAMPORTS] reads the account's lamport balance straight out of the input region. There is no separate 'fetch account' step because the runtime already mapped account 0's header into the same memory r1 points at.

ldxdw r4, [r1 + INSTRUCTION_DATA] reads the caller's floor from the ix data.

jlt r3, r4, below_floor is the entire check. jlt is unsigned less-than: if the account holds less than the floor, jump to the failure exit. Equal passes because jlt is strict.

lines 15-16

Happy path exit

  mov64 r0, 0  exit

Seven instructions on the happy path, 7 CU total. balance_floor is one of the cheapest guards because the runtime hands us pre-decoded lamports at a fixed offset. No parsing, no syscall, no function calls. Just a load and a compare.

lines 18-30

Two failure paths

below_floor:  lddw r1, msg_below  mov64 r2, 11  call sol_log_  mov64 r0, 1  exit bad_ix_data:  lddw r1, msg_bad  mov64 r2, 11  call sol_log_  mov64 r0, 2  exit

below_floor logs 'below floor' (11 bytes), exits 1 (condition failed). The SDK reads 'below floor' from the log and reports a friendly error to the user.

bad_ix_data logs 'bad ix data' (11 bytes), exits 2 (malformed ix data). Code 2 means the caller's instruction was malformed (probably a client bug), not that the user failed the balance check.

There is no exit 3 here. balance_floor accepts ANY account at position 0: a Solana program account, a system account, a token account, a wallet, whatever. The guard just reads the lamport field at offset 0x50 and trusts the caller about what KIND of account this is. If you point it at a non-existent address, the runtime materializes a zero-lamports placeholder and the check fails with exit 1.

lines 32-34

Read-only string table

.rodata  msg_below: .ascii "below floor"  msg_bad:   .ascii "bad ix data"

Two log strings live in .rodata, baked into the .so at link time. lddw at runtime loads their addresses; the bytes themselves are part of the compiled program image.

Exit codes

Exit codes for this guard
codenamelog line
0Success(no log)
1ConditionFailedbelow floor
2BadInstructionDatabad ix data