XRP Ledger Standards

XLS-0102
Draft
  title: WASM VM
  description: WebAssembly VM integration into rippled
  author: Mayukha Vadari (@mvadari), Peng Wang (@pwang200), Oleksandr Pidskopnyi (@oleks_rip), David Fuelling (@sappenin)
  proposal-from: https://github.com/XRPLF/XRPL-Standards/discussions/303
  status: Draft
  category: Amendment
  created: 2025-08-08
  updated: 2026-07-24

WASM VM Configuration

Abstract

This document describes the integration of WebAssembly (WASM) into the XRP Ledger as a secure and deterministic execution environment for smart contract logic. WASM-based execution allows developers to write custom logic in a wide range of languages, compile to a portable binary format, and run within a sandboxed virtual machine governed by the consensus process. The standard outlines the interface, constraints, and security model for deploying, invoking, and validating WASM subroutines on-ledger, while ensuring compatibility with existing ledger primitives and transaction flow.

1. Overview

This document addresses several parts of the WASM integration into rippled:

  • The implementation chosen
  • The gas model
  • The set of host functions
  • Security measures

This feature does not (directly) involve any new transactions, ledger objects, or RPCs. It will be gated by the SmartEscrow amendment. Any modification to the details in this spec will require an amendment, as it will affect transaction processing (e.g. success/failure of an EscrowFinish transaction for a Smart Escrow).

1.1. How the WASM Engine Integrates into rippled

image

Using Smart Escrows as an example:

  1. Process the transaction until it has done everything it needs to do before processing anything that requires the WASM engine (in this case, running the Bytecode to determine if the escrow is finishable).
  2. Enter the WASM engine, where the WASM environment is set up to run the code.
  3. Run the WASM code, using host functions to fetch on-ledger information.
  4. Return the output (whether or not the escrow can be finished) to the transaction processing engine, and continue onwards with the rest of the transaction code.

1.2. Background: What is a “Host Function”?

A host function is a function expressed outside WebAssembly but passed to a module as an import. They’re somewhat analogous to precompiles in the EVM world.

In other words, it’s basically an API call that fetches/interacts with data or native compute outside of the WASM VM.

1.3. Background: WASM Native Types

There are only 4 native types in the WASM spec: i32 (a signed 32-bit integer), i64 (a signed 64-bit integer), f32 (a 32-bit floating point number), and f64 (a 64-bit floating point number). However, the floating point numbers use a different encoding from what rippled uses.

So essentially, we only have i32 and i64 in terms of useful types. Every parameter and return type must be represented as these two types. This is manifested as pointers and lengths. Note that any language that has full support for XRPL extensions will have helper functions to abstract away most of the complexity (especially involving pointers and lengths).

2. VM Runtime Choice

While WebAssembly has a core specification, different runtimes have flexibility in how they implement certain features that are not a part of the formal specification. For example, not all WASM runtimes can easily be embedded in a C++ project (such as rippled).

The most relevant part for the purpose of consensus is the gas cost for any operation or function. Different implementations may have different gas costs for executing a given function, due to implementation differences - e.g. some calculate gas costs by inserting additional instructions, while others have a counter in the VM logic. For instance, one basic Smart Escrow function cost 110 gas to run with WasmEdge, while it only cost 5 gas with Wasmi. This would cause consensus issues if the computation limit was set at 100, for example - one runtime would succeed, while the other would fail.

There are other metrics that are important as well, such as performance considerations. See Appendix A for the full analysis comparing different runtimes.

2.1. Gas

Gas consumption is determined by the WASM runtime used for execution. Different implementations may use different metering strategies, which yields different gas costs for identical WASM code. This blog post discusses several of the different strategies.

Gas will accumulate from:

  • Execution of individual WASM instructions
  • Memory operations (e.g. grow_memory)
  • Calling host functions (e.g. accessing ledger data)

Exceeding the provided gas budget triggers immediate execution halting, with a deterministic failure consistent across implementations.

3. Execution Limits

The XRPL cannot allow unbounded execution, as there is a time limit for ledgers to close in order for consensus to execute in a timely manner. There are three methods of ensuring that the WASM code does not take too many resources:

  • A size limit to the WASM code
  • A computation limit
  • A price for each unit of gas

All of these parameters will be UNL-votable, so that they do not need a separate amendment to be modified.

4. Memory Management Strategies

WebAssembly 1.0 (which is the WASM profile that the XRPL will support) does not include built-in memory management - there is no garbage collector or automatic heap allocation. Instead, memory is a contiguous linear buffer that can only grow (in fixed 64 KiB pages) and never shrink. That means WASM code must allocate and manage its own memory, typically via a custom allocator or language runtime, and explicitly track when memory is no longer needed. While there is progress on this front with WasmGC, it does not have full-fledged tooling support yet, and is currently only really useful for browser applications of WASM.

This is a bit of a problem for host functions, since data has to go back and forth between the caller (WASM dev) and the engine (rippled). Some data (e.g. parameters) may be generated on the WASM side, while some data (e.g. the return data) may be generated on the rippled side.

Therefore, in this design, the caller is responsible for allocating memory in advance, and must reuse or deallocate memory manually. See Appendix B for alternative designs that were considered and rejected.

4.1. Memory Limits

There are:

  • 1 MiB limit, per host function call, on total data transfer across the WASM boundary (between rippled and WASM code)
  • 1 KiB limit on the amount of data that can be read from or written to the ledger in a single host function call

5. Extension Host Functions

This section introduces WASM host functions for extensions on the XRP Ledger, enabling WASM bytecode (in an extension or smart contract) to securely interact with ledger data and the ledger’s native features. These functions provide controlled access to ledger state, transaction execution, and XRPL primitives while maintaining efficiency and security.

WASM code, whether in an extension or a smart contract, needs access to XRPL ledger data in order to be useful. A host function allows that access, in a secure way. Host functions can also be used to save gas/compute in WASM, as they can perform those same functions in C++ code instead, which will likely be more performant.

Some examples from XLS-100:

  • A notary escrow extension needs access to the triggering EscrowFinish transaction (to know who is sending the transaction).
  • An escrow checking for KYC needs access to ledger state, to determine if the destination has a given credential.

This spec only covers Smart Escrow host functions at this time.

These host functions will be accessible from extensions and smart contracts.

Note: all these functions return an i32, unless otherwise noted (or there is no buffer parameter). If the value is positive, it's a length. If it's negative, it's an error code.

5.1. General Ledger Data

This section includes ledger header data, amendments, and fees.

Function Signature Description Gas Cost
ldgr_index(
out_buff_ptr: i32,
out_buff_len: i32
)
Get the index number of the last ledger. 60
parent_ldgr_time(
out_buff_ptr: i32,
out_buff_len: i32
)
Get the time (in Ripple Time) of the last ledger. 60
parent_ldgr_hash(
out_buff_ptr: i32,
out_buff_len: i32
)
Get the hash of the last ledger. 60
amendment_enabled(
amendment_ptr: i32,
amendment_len: i32
)
Check if a given amendment is enabled. 100
base_fee(
out_buff_ptr: i32,
out_buff_len: i32
)
Get the current transaction base fee. 60

5.2. Current Ledger Object data

The current ledger object is the ledger object that the extension lives on - for Smart Escrows that's an Escrow object.

Function Signature Description Gas Cost
home_le_field(
field: i32,
out_buff_ptr: i32,
out_buff_len: i32
)
Get a top-level field from the ledger object that the extension is on. 70
home_le_inner(
locator_ptr: i32,
locator_len: i32,
out_buff_ptr: i32,
out_buff_len: i32
)
Get an inner field from the ledger object that the extension is on. 110
home_le_arr_len(
field: i32
)
Get the length of an array field on the ledger object that the extension is on. 40
home_le_inner_arr_len(
locator_ptr: i32,
locator_len: i32
)
Get the length of an inner array field on the ledger object that the extension is on. 70
5.2.1. Locators

A Locator allows a WASM developer to reference any field in any object (even inner fields) by specifying a slot_num (1 byte); a locator_field_type (1 byte); then one of an sfield (4 bytes) or an index (4 bytes).

5.3. Current Transaction Data

The current transaction is the EscrowFinish that is executing the WASM logic

Function Signature Description Gas Cost
tx_field(
field: i32,
out_buff_ptr: i32,
out_buff_len: i32
)
Get a top-level field from the transaction that triggered the extension. 70
tx_inner(
locator_ptr: i32,
locator_len: i32,
out_buff_ptr: i32,
out_buff_len: i32
)
Get an inner field from the transaction that triggered the extension. 110
tx_arr_len(
field: i32
)
Get the length of an array field from the transaction that triggered the extension. 40
tx_inner_arr_len(
locator_ptr: i32,
locator_len: i32
)
Get the length of an inner array field from the transaction that triggered the extension. 70

5.4. Any Ledger Object Data

Fetch data from any other ledger object

Function Signature Description Gas Cost
cache_le(
index_ptr: i32,
index_len: i32,
cache_num: i32
)
Cache a ledger object so that it can be used later. 5000
le_field(
cache_num: i32,
field: i32,
out_buff_ptr: i32,
out_buff_len: i32
)
Get a top-level field from any ledger object. 70
le_inner(
cache_num: i32,
locator_ptr: i32,
locator_len: i32,
out_buff_ptr: i32,
out_buff_len: i32
)
Get an inner field from any ledger object. 110
le_arr_len(
cache_num: i32,
field: i32
)
Get the length of an array field from any ledger object. 40
le_inner_arr_len(
cache_num: i32,
locator_ptr: i32,
locator_len: i32
)
Get the length of an inner array field from any ledger object. 70

5.5. Ledger Entry IDs

A ledger entry ID is a unique hash that represents a ledger object on the XRP Ledger. It is a 256-bit hash, constructed from unique identifiers for an object. For example, an AccountRoot's hash is constructed from its AccountID, and an Oracle's hash is constructed from its Owner and DocumentID.

Function Signature Description Gas Cost
accountroot_id(
account_ptr: i32,
account_len: i32,
out_buff_ptr: i32,
out_buff_len: i32
)
Calculate an AccountRoot's index from its pieces. 350
amm_id(
issue1_ptr: i32,
issue1_len: i32,
issue2_ptr: i32,
issue2_len: i32,
out_buff_ptr: i32,
out_buff_len: i32
)
Calculate an AMM’s index from its pieces. 450
check_id(
account_ptr: i32,
account_len: i32,
sequence_ptr: i32,
sequence_len: i32,
out_buff_ptr: i32,
out_buff_len: i32
)
Calculate a Check's index from its pieces. 350
credential_id(
subject_ptr: i32,
subject_len: i32,
issuer_ptr: i32,
issuer_len: i32,
cred_type_ptr: i32,
cred_type_len: i32,
out_buff_ptr: i32,
out_buff_len: i32
)
Calculate a Credential's index from its pieces. 350
delegate_id(
account_ptr: i32,
account_len: i32,
authorize_ptr: i32,
authorize_len: i32,
out_buff_ptr: i32,
out_buff_len: i32
)
Calculate a Delegate's index from its pieces. 350
deposit_preauth_id(
account_ptr: i32,
account_len: i32,
authorize_ptr: i32,
authorize_len: i32,
out_buff_ptr: i32,
out_buff_len: i32
)
Calculate a DepositPreauth's index from its pieces. 350
did_id(
account_ptr: i32,
account_len: i32,
out_buff_ptr: i32,
out_buff_len: i32
)
Calculate a DID's index from its pieces. 350
escrow_id(
account_ptr: i32,
account_len: i32,
sequence_ptr: i32,
sequence_len: i32,
out_buff_ptr: i32,
out_buff_len: i32
)
Calculate an Escrow's index from its pieces. 350
trustline_id(
account1_ptr: i32,
account1_len: i32,
account2_ptr: i32,
account2_len: i32,
currency_ptr: i32,
currency_len: i32,
out_buff_ptr: i32,
out_buff_len: i32
)
Calculate a trustline’s index from its pieces. 400
mpt_issuance_id(
issuer_ptr: i32,
issuer_len: i32,
sequence_ptr: i32,
sequence_len: i32,
out_buff_ptr: i32,
out_buff_len: i32
)
Calculate an MPTIssuance’s index from its pieces. 350
mptoken_id(
mptid_ptr: i32,
mptid_len: i32,
holder_ptr: i32,
holder_len: i32,
out_buff_ptr: i32,
out_buff_len: i32
)
Calculate an MPToken’s index from its pieces. 500
nft_offer_id(
account_ptr: i32,
account_len: i32,
sequence_ptr: i32,
sequence_len: i32,
out_buff_ptr: i32,
out_buff_len: i32
)
Calculate an NFTOffer's index from its pieces. 350
offer_id(
account_ptr: i32,
account_len: i32,
sequence_ptr: i32,
sequence_len: i32,
out_buff_ptr: i32,
out_buff_len: i32
)
Calculate an Offer's index from its pieces. 350
oracle_id(
account_ptr: i32,
account_len: i32,
document_id_ptr: i32,
document_id_len: i32,
out_buff_ptr: i32,
out_buff_len: i32
)
Calculate an Oracle's index from its pieces. 350
paychan_id(
account_ptr: i32,
account_len: i32,
destination_ptr: i32,
destination_len: i32,
sequence_ptr: i32,
sequence_len: i32,
out_buff_ptr: i32,
out_buff_len: i32
)
Calculate a PayChannel’s index from its pieces. 350
permissioned_domain_id(
account_ptr: i32,
account_len: i32,
sequence_ptr: i32,
sequence_len: i32,
out_buff_ptr: i32,
out_buff_len: i32
)
Calculate a PermissionedDomain’s index from its pieces. 350
signers_id(
account_ptr: i32,
account_len: i32,
out_buff_ptr: i32,
out_buff_len: i32
)
Calculate a SignerListSet's index from its pieces. 350
ticket_id(
account_ptr: i32,
account_len: i32,
sequence_ptr: i32,
sequence_len: i32,
out_buff_ptr: i32,
out_buff_len: i32
)
Calculate a Ticket's index from its pieces. 350
vault_id(
account_ptr: i32,
account_len: i32,
sequence_ptr: i32,
sequence_len: i32,
out_buff_ptr: i32,
out_buff_len: i32
)
Calculate a Vault’s index from its pieces. 350

The singleton indexes (e.g. Amendments) are a bit unnecessary to include, as a dev can simply copy the index directly instead. They will be included as constants in xrpl-wasm-stdlib as well.

The directory indexes and NFTokenPage were not included, since they are a bit more complex to parse through and it seemed unnecessary for now. These can always be added in the future.

5.6. NFTs

Fetch information about NFTs.

Function Signature Description Gas Cost
nft_uri(
owner_ptr: i32,
owner_len: i32,
nft_id_ptr: i32,
nft_id_len: i32,
out_buff_ptr: i32,
out_buff_len: i32
)
Get an NFT URI from its owner and ID. 5000
nft_issuer(
nft_id_ptr: i32,
nft_id_len: i32,
out_buff_ptr: i32,
out_buff_len: i32
)
Extract the NFT issuer from the NFT ID. 70
nft_taxon(
nft_id_ptr: i32,
nft_id_len: i32,
out_buff_ptr: i32,
out_buff_len: i32
)
Extract the NFT taxon from the NFT ID. 60
nft_flags(
nft_id_ptr: i32,
nft_id_len: i32
)
Extract the NFT flags from the NFT ID. 60
nft_xfer_fee(
nft_id_ptr: i32,
nft_id_len: i32
)
Extract the NFT transfer fee from the NFT ID. 60
nft_serial(
nft_id_ptr: i32,
nft_id_len: i32,
out_buff_ptr: i32,
out_buff_len: i32
)
Extract the NFT serial from the NFT ID. 60

5.7. Utils

Miscellaneous utility functions.

Function Signature Description Gas Cost
check_sig(
message_ptr: i32,
message_len: i32,
signature_ptr: i32,
signature_len: i32,
pubkey_ptr: i32,
pubkey_len: i32,
)
Check the validity of a signature. Returns a 0 for invalid and 1 for valid. Supports both ED25519 and SECP256K1. 35000
sha512_half(
data_ptr: i32,
data_len: i32,
out_buff_ptr: i32,
out_buff_len: i32
)
Calculate the sha512 half hash of provided data. 1500

5.8. Floats

Helper functions for performing floating point arithmetic via xrpld. These are used for any calculation requiring XRPL's decimal floating point format, including IOU amounts, lending protocol math, fee calculations, or arbitrary numeric operations within a smart contract.

All float buffers are exactly 12 bytes and are opaque to contracts (see §5.8.1).

The rounding_modes parameter accepts: 0 (round to nearest, ties to even), 1 (toward zero), 2 (downward, floor), 3 (upward, ceiling).

Function Signature Description Gas Cost
float_from_uint(
in_uint_ptr: i32,
in_uint_len: i32,
out_buf: i32,
out_len: i32,
rounding_modes: i32
)
Create a float in xrpld format from a 64-bit unsigned integer with little-endian encoding. 130
float_from_iou_value(
in_buf: i32,
in_len: i32,
out_buf: i32,
out_len: i32
)
Load a float from the 8-byte IOU amount field of a serialized STAmount. 150
float_from_mant_exp(
mantissa: i64,
exponent: i32,
out_buf: i32,
out_len: i32,
rounding_modes: i32
)
Create a float in xrpld format from a mantissa and an exponent. 100
float_to_mant_exp(
in_buf: i32,
in_len: i32,
mantissa_out_buf: i32,
mantissa_out_len: i32,
exponent_out_buf: i32,
exponent_out_len: i32
)
Extract the mantissa (i64) and exponent (i32) from a float, both little-endian. 130
float_cmp(
in_buf1: i32,
in_len1: i32,
in_buf2: i32,
in_len2: i32
)
Compare two floats in xrpld format. 80
float_add(
in_buf1: i32,
in_len1: i32,
in_buf2: i32,
in_len2: i32,
out_buf: i32,
out_len: i32,
rounding_modes: i32
)
Add two floats in xrpld format. 160
float_sub(
in_buf1: i32,
in_len1: i32,
in_buf2: i32,
in_len2: i32,
out_buf: i32,
out_len: i32,
rounding_modes: i32
)
Subtract two floats in xrpld format. 160
float_mul(
in_buf1: i32,
in_len1: i32,
in_buf2: i32,
in_len2: i32,
out_buf: i32,
out_len: i32,
rounding_modes: i32
)
Multiply two floats in xrpld format. 300
float_div(
in_buf1: i32,
in_len1: i32,
in_buf2: i32,
in_len2: i32,
out_buf: i32,
out_len: i32,
rounding_modes: i32
)
Divide two floats in xrpld format. 300
float_pow(
in_buf: i32,
in_len: i32,
pow: i32,
out_buf: i32,
out_len: i32,
rounding_modes: i32
)
Compute the nth power of a float in xrpld format. 5500

The little-endian encoding above applies only to the raw-integer buffers of float_from_uint and float_to_mant_exp. It does not apply to the XFloat buffer itself (the in_buf/out_buf arguments on every other function above), which is always big-endian per §5.8.3, nor to float_from_iou_value's in_buf, which carries the on-ledger STAmount IOU value encoding (also big-endian, unchanged from the existing ledger format). The mantissa/exponent arguments of float_from_mant_exp and the pow argument of float_pow are passed directly as WASM i64/i32 values rather than through a memory buffer, so no byte order applies to them.

5.8.1. The XFloat Type

Any floating point type in xrpld WASM is an opaque 12-byte (96-bit) buffer referred to in this document and in SDKs as an XFloat. The Number class in xrpld is the core decimal floating-point type used throughout the ledger; all floating point arithmetic is delegated to it via host functions.

Smart contracts SHOULD NOT inspect, decode, or construct XFloat bytes directly. All operations should instead go through the host functions defined in §5.8. Bypassing host functions is gas-inefficient — each host call is priced to amortize the cost of the operation, and hand-rolling arithmetic in WASM bytecode incurs more gas for less precision. A contract that reads or writes the individual bytes of an XFloat buffer is also relying on an implementation detail that may change, and could produce incorrect or undefined behavior if it does. The buffer should be allocated, passed to a host function, and discarded — nothing else.

[!WARNING] Do not persist XFloat bytes. Contracts MUST NOT write XFloat buffers into contract storage (e.g., the data field of a smart feature). The 12-byte encoding is an in-memory convention tied to today's float host functions. Per §5.11 rule 3, that encoding can never change for these functions — but a future need for a different layout would ship as a new host function under a new name, and bytes persisted under today's convention would not be readable by it.

If a contract needs to persist an XFloat across invocations, it should store the mantissa and exponent as separate integers in a contract-defined format, then reconstruct the XFloat at runtime using float_from_mant_exp. For example:

// Persisting: decompose into primitive integers and write to contract data
let exponent: i32 = /* obtained from contract logic */;
let mantissa: i64 = /* obtained from contract logic */;
// Store exponent (4 bytes) and mantissa (8 bytes) in your own layout

// Restoring: reconstruct from stored integers
let mut f = [0u8; 12]; float_from_mant_exp(mantissa, exponent, f.as_mut_ptr(), 12, 0 /* TO_NEAREST */);

This approach uses only stable primitive types (i32, i64) and is completely independent of any future changes to the XFloat binary layout.

5.8.2. XFloat Motivation

WASM code running on the XRPL — whether in a Smart Escrow, a Smart Contract, or any other Smart Feature — needs to perform correct decimal arithmetic. This need arises in many contexts: computing with fungible token amounts (IOUs), implementing lending protocols with interest and collateral ratios, calculating fees, and more.

Implementing xrpld floating-point arithmetic correctly is genuinely hard. Correct rounding, normalization, overflow handling, and edge-case behavior require a carefully engineered implementation. Implementing this correctly in WASM from scratch is not a reasonable expectation for contract developers, and cannot be practically verified or guaranteed. By delegating all arithmetic to xrpld's Number class via host functions, contracts get a battle-tested implementation that is known to be correct for XRPL's numeric domain, even across amendment changes.

Note that the XRPL WASM VM does not enable the WASM floating-point instruction set (i.e., f32/f64 ops are unavailable to contracts). This means native IEEE 754 arithmetic is not an option regardless of determinism concerns. Contracts that need fixed-point arithmetic independent of the XFloat host functions (for example, to work with integer ratios or basis points) should consider crates like the fixed Rust crate, which performs fixed-point math entirely in integer instructions and is fully compatible with the no_std, wasm32v1-none build target.

5.8.3. XFloat Serialization Format

This section documents the XFloat encoding for xrpld implementers and tooling authors.

XFloats use a binary encoding inspired by, but not identical to, XRPL's STNumber serialization:

  • Layout: 12 bytes total — 8-byte big-endian signed mantissa followed by a 4-byte big-endian signed exponent.
  • No type prefix: The buffer contains only the 12 payload bytes
  • Consensus-compatible: Produced and consumed exclusively by xrpld's host function implementations

Serialization Layout (96 bits / 12 bytes):

[Signed Mantissa: 8 bytes (i64, big-endian)][Signed Exponent: 4 bytes (i32, big-endian)]

Field Descriptions:

  • Mantissa (bytes 0–7): Signed 64-bit integer (i64), big-endian, in the range −(2^63−1) to 2^63−1. Together with the exponent it represents the value mantissa × 10^exponent. The mantissa written here is the value returned by xrpld's Number::mantissa() accessor, i.e. the external view of the number, not the internal representation. Internally, xrpld's Number normalizes its unsigned mantissa to 10^18 ≤ |m| < 10^19 in the default large-scale mode (active when the SingleAssetVault or LendingProtocol amendment is enabled), or to 10^15 ≤ |m| < 10^16 in the legacy small-scale mode. When the internal mantissa exceeds i64::MAX (2^63−1), and only then, the accessor divides it by 10 and increments the exponent by 1; the dropped digit is always zero, so the conversion is lossless. As a result, the on-wire magnitude |mantissa| of a normalized non-zero large-scale value satisfies either 10^18 ≤ |mantissa| ≤ 2^63−1 (no digit dropped) or 922,337,203,685,477,581 ≤ |mantissa| < 10^18 (one zero digit dropped). Tooling must not assume a fixed number of significant digits.
  • Exponent (bytes 8-11): Signed 32-bit integer (i32), big-endian. Represents the power of 10 applied to the mantissa.

Special Values:

  • Zero: Exponent and mantissa both 0 — all 12 bytes are 0x00.
  • Null / uninitialized: A distinct state used internally by xrpld with no defined byte pattern.

Relationship to On-Ledger Formats:

The on-ledger wire format of any floating point numbers is unchanged. However, the byte layout is frequently incompatible with XFloat numbers accepted by float-prefixed host functions. For example, a fungible token amount in an STAmount field is not an XFloat. In particular:

[STAmount amount field: 8 bytes][Currency: 20 bytes][Issuer: 20 bytes] = 48 bytes total

The 12-byte XFloat format is strictly an in-memory buffer convention for passing values to and from WASM host functions. Values stored in ledger objects continue to use their existing serialization formats; the host function float_from_iou_value bridges the IOU amount format to WASM floating point numbers. STNumber values are decoded in Rust by xrpl-wasm-stdlib — for the IOU-precision case, the decoded 8-byte value is layout-identical to an IouNumber and is passed to float_from_iou_value directly, without a dedicated host function.

5.8.4. XFloat Example Usage
#![no_std]
#![no_main]

#[unsafe(no_mangle)]
pub extern "C" fn finish() -> i32 {
  // Load an XFloat from a serialized STAmount (IOU variant, 8 bytes)
  let iou_amount_bytes = [0u8; 8]; // obtained from transaction or ledger object
  let mut xfloat_a = [0u8; 12];
  if float_from_iou_value(
    iou_amount_bytes.as_ptr(), 8,
    xfloat_a.as_mut_ptr(), 12,
  ) < 0 {
    return 0; // error
  }

  // Convert an integer to XFloat (exponent 0 → value is exactly the mantissa)
  let mut xfloat_b = [0u8; 12];
  if float_from_mant_exp(100, 0, xfloat_b.as_mut_ptr(), 12, 0) < 0 {
    return 0;
  }

  // Add the two floats
  let mut xfloat_result = [0u8; 12];
  if float_add(
    xfloat_a.as_ptr(), 12,
    xfloat_b.as_ptr(), 12,
    xfloat_result.as_mut_ptr(), 12,
    0, // TO_NEAREST
  ) < 0 {
    return 0;
  }

  // Extract mantissa and exponent; if exponent is 0, mantissa is the integer value
  let mut mantissa_result = [0u8; 8];
  let mut exponent_result = [0u8; 4];
  if float_to_mant_exp(
    xfloat_result.as_ptr(), 12,
    mantissa_result.as_mut_ptr(), 8,
    exponent_result.as_mut_ptr(), 4,
  ) < 0 {
    return 0;
  }

let mantissa:i64 = i64::from_le_bytes(mantissa_result);
let exponent:i32 = i32::from_le_bytes(exponent_result);

  1 // Success
}
5.8.5. XFloat Binary Format Reference

This reference is intended for xrpld implementers and tooling authors (debuggers, explorers, spec verification).

XFloat layout (12 bytes):

Offset  Size  Type   Description
------  ----  -----  -----------
0       8     i64    Signed mantissa, big-endian
8       4     i32    Signed exponent, big-endian

Zero value (12 bytes):

00 00 00 00 00 00 00 00   <- mantissa: 0
00 00 00 00   <- exponent: 0

Example: positive value with exponent -15, mantissa 10^15:

00 03 8D 7E A4 C6 80 00   <- mantissa: 1,000,000,000,000,000 (10^15)
FF FF FF F1   <- exponent: -15 (i32 big-endian: 0xFFFFFFF1)

5.9. Trace

Output debug info to the rippled debug log (if trace logging is enabled). The maximum size of data that can be passed into these functions is 1024 bytes (attempting to pass in more will trigger an error).

Each of these host functions will return 0 on success and a negative value on failure.

Function Signature Description Gas Cost
trace(
msg_ptr: i32,
msg_len: i32,
data_ptr: i32,
data_len: i32,
as_hex: i32
)
A logging helper function. 500
trace_num(
msg_ptr: i32,
msg_len: i32,
number: i64
)
A logging helper function for numbers. 500
trace_xfloat(
msg_ptr: i32,
msg_len: i32,
xfloat_ptr: i32,
xfloat_len: i32
)
A logging helper function for floats in rippled format. 500
trace_acct(
msg_ptr: i32,
msg_len: i32,
account_ptr: i32,
account_len: i32
)
A logging helper function for accounts. 500
trace_amt(
msg_ptr: i32,
msg_len: i32,
amount_ptr: i32,
amount_len: i32
)
A logging helper function for amounts. 500

5.10. Updating Fields

Update on-chain data associated with the WASM code.

This section is the only section of functions that will likely be different for each Smart Feature. Each may have its own way of storing data.

Function Signature Description Gas Cost
set_data(
data_ptr: i32,
data_len: i32
)
Update the Data field in the ledger object that hosts the WASM code, e.g. a Smart Escrow. 1000

5.11. Host Function Versioning Rules

The following rules govern the lifecycle of all host functions in this specification and must be respected by all implementations:

  1. New host functions MAY be added at any time without breaking existing contracts. Contracts that do not call a new function are unaffected.
  2. Host functions MAY be deprecated with appropriate notice, but deprecated functions MUST remain callable for backward compatibility. Deployed contracts may rely on any host function that was available at deployment time. Deprecation does not remove or change the function — it signals to new contract authors that a function is discouraged, so documentation and tooling (e.g. xrpl-wasm-stdlib) can steer new development away from it even though it remains available for existing contracts.
  3. Host functions MUST be permanently backwards compatible. Once a host function is deployed, its name, parameter types, parameter order, and observable behavior for all previously valid inputs are permanently fixed. This includes buffer sizes, since contracts hardcode allocation sizes (e.g., 20 bytes for an account ID, 12 bytes for an XFloat). If a change cannot be made backwards compatibly — for example, a required buffer size grows — a new host function with a different name MUST be introduced instead.

These rules ensure that WASM code compiled and deployed today will continue to execute correctly on future versions of the platform.

6. Rationale

6.1. Interpreted Wasmi Runtime

Different WASM runtimes meter gas differently, so the same code can produce different gas costs across implementations — a consensus hazard. Fixing the runtime (Wasmi), its version, and an interpreted compile mode guarantees identical, deterministic gas costs on every validator. Interpretation also avoids the larger attack surface and platform-dependent behavior of JIT/AOT compilation. See Appendix A for the full comparison of runtimes and compilation modes.

6.2. Caller-Allocated Memory

WebAssembly 1.0 has no built-in memory management, and data must cross the boundary between WASM code and rippled in both directions. Making the caller responsible for allocating buffers in advance keeps the host interface simple and avoids the pitfalls of the host-managed and callback-based designs described in Appendix B.

6.3. Host Functions Rather Than Direct Ledger Access

WASM code can only interact with the ledger through an explicitly defined host function interface. This keeps user code sandboxed, bounds what data is visible, and moves expensive operations into native C++ code where they are cheaper and their gas cost can be priced deterministically.

6.4. A Custom Float Type (XFloat) Rather Than Native WASM Floating Point

Native floating point is a source of non-determinism across platforms, so it is disallowed; float arithmetic is instead provided by host functions over an opaque buffer type (see §5.8 and FAQ C.5).

6.5. A Backwards-Compatible Host-Function ABI, With Amendment-Gated Additions

Deployed contract binaries cannot be updated, so a contract compiled today must run correctly on every future version of rippled. Host functions are therefore never changed incompatibly once shipped — new behavior arrives as new functions gated by amendments (see §5.11 and §7.5).

6.6. UNL-Votable Execution Limits

The code size limit, computation limit, and gas price are votable parameters rather than hard-coded constants, so they can be tuned without requiring a new amendment for every adjustment (see §3).

7. Security

7.1. Consensus

The WASM VM and spec guarantees that all WASM code will run identically on all machines (though, of course, a lot of testing will be done to ensure that this is the case).

WebAssembly is designed with deterministic execution in mind, and the specification ensures that properly constrained WASM code will produce the same output across all (compliant) runtimes. This XLS relies on those guarantees to ensure that all validators in the XRP Ledger network reach the same result when executing WASM code as part of transaction processing.

To that end:

  • The runtime environment is fixed across all validator nodes, with an agreed-upon WebAssembly implementation (Wasmi), the Wasmi version, and a deterministic configuration (interpreted compile mode).
  • Non-deterministic WASM features, such as floating point operations, access to time, randomness, or host system I/O, are explicitly disallowed or omitted from the runtime.
  • A fixed, deterministic gas cost model is applied to all instructions, with enforced gas limits and metering to ensure bounded execution.

To ensure that all of this is the case, thorough testing across platforms and architectures will be conducted. Any divergence will be considered a critical consensus-breaking bug.

7.2. Mitigations for Bugs

If there happens to be a bug in the WASM execution layer, the UNL can shut down all usage of WASM code by setting the computation limit to 0.

7.3. Data Security

User-provided WASM code is executed within a strict sandbox. It has no access to system-level resources and can only interact with the XRP Ledger via an explicitly defined host function interface. These host functions enforce strict boundaries on what ledger data is visible and what operations are permitted. For example, there is no way for user-provided WASM code to directly modify a ledger object (to e.g. transfer XRP between accounts without permission).

A new WASM VM instance will be created for each WASM module execution. This ensures that there is no state that can be leaked between different executions, and that memory cannot be corrupted between runs.

WASM code cannot directly traverse arbitrary ledger directories or iterate through global ledger state. All access must be via bounded, predefined inputs (e.g., indexes or account IDs passed into the subroutine). This design ensures that malicious WASM code cannot manipulate or exfiltrate ledger state beyond the narrow scope allowed by the host API.

7.4. Resource Limiting

As discussed above, there is a strict gas limit and exceeding it will result in execution being immediately terminated with an exception.

Additionally, memory and stack usage are tightly constrained - the linear memory size is bounded to a fixed number of pages, and stack depth is capped to prevent runaway recursion or stack overflows.

These constraints prevent denial-of-service attacks and ensure that WASM execution remains fast and predictable, without any WASM-related transaction taking more than its share of rippled resources.

7.5. Future-Proofing

The host functions defined by this spec form a stable ABI. Once a host function is shipped under an amendment, its name, semantics, parameter list, and return type must remain backwards compatible forever, as there may always be a deployed Smart Escrow (or other extension) that depends on it. The following kinds of changes are permitted, but must be gated by amendments:

  • Adding a new host function. Existing extensions are unaffected; new extensions opt in by importing the new name only after the amendment is enabled.
  • Adjusting the gas cost of an existing host function. The function's signature and behavior are unchanged; only the metered cost moves.

Backwards-incompatible changes to a host function's observable behavior are not on this list — per §5.11 rule 3, any such change requires introducing a new host function with a new name; the amendment gates adoption of the new function, not a change to the old one.

Updates to the wasmi package may also need to be gated by an amendment - every update will need to be tested for the potential of breaking changes.

For example, this is what it might look like to add a new host function:

WASM_IMPORT_FUNC2(i, didindex, "did_index", hfs,     350);
WASM_IMPORT_FUNC2(i, escrowindex, "escrow_index", hfs,       350);
if (rules.enabled(featureLendingProtocol))
    WASM_IMPORT_FUNC2(i, loanindex, "loan_index", hfs,     350);

(in WasmVM.cpp)

This ensures that smart escrows cannot use the loan_index host function at all before the LendingProtocol amendment is activated, as the amendment process ensures that all nodes and validators have the code before it is run.

Appendix

Appendix A: Other WASM VMs Considered

A.1. The Different WASM Compilation Modes

A.1.1. AOT (Ahead of Time)

AOT compiles WASM straight to native machine code ahead of time. If we were to use this compilation mode, users would have to store native machine code on the ledger.

This isn’t useful for our needs, as the compiled AOT code will be architecture-specific.

Can we figure out a way to support AOT? Possibly, but likely not without restricting rippled hardware to certain CPU architectures, and even then likely only one. Alternatively, to support AOT we would need to force WASM developers to supply variants that can run on any native architectures supported by rippled. Even if we imagine limiting rippled to 3 architectures, that would mean every smart contract developer would need to supply 3 different versions of their WASM, which would be wasteful from a space perspective. Last but not least, limiting rippled to 3 architectures seems counterproductive to decentralization.

A.1.2. JIT (Just-In-Time)

JIT compiles essentially as the code is run, or just before. This allows for additional caching and optimizations.

However, there are a few issues with JIT. From this Stellar blog, JIT-based VMs are also not as secure and are susceptible to “JIT Bombs.” It has longer start times and greater memory usage. Mac also does not support JIT. The use of JIT may also result in different gas costs on different machines depending on what is in the cache, which would be a consensus-breaking change. Therefore, JIT cannot be used for our needs.

A.1.3. Interpreted

Interpreted mode just runs the code, like a REPL.

We decided on this mode because, well, the other two don't work for our needs, even though they're often more performant.

A.2. The Different WASM Implementations

The 5 WASM VM implementations we investigated were:

A.2.1. Initial Investigation

image

Based on these findings, we narrowed down the search to WasmEdge and WAMR, and we then conducted further performance testing and analysis on those two options.

A.2.1.1. Performance Analysis

image4image2image1

These graphs clearly show that WAMR is much more performant.

A.2.2. Revisit

Several months later, we revisited the VM runtime decision. We found that Wasmi was a better fit for our needs than WAMR. See this blog post for more details.

Appendix B: Memory Management Strategies Considered

Options:

  1. Caller-Allocated: The contract developer (on the WASM side) allocates fixed size arrays for returning data. The user knows the pointer and the length.
  2. This is really easy to implement, but means that the WASM dev needs to do their own memory allocation.
  3. Host-Allocated: The host (rippled) allocates WASM memory in host functions and passes the pointer and the length to the WASM program.
  4. This is super easy to use for devs, as they don’t need to worry about allocation. However, more research is needed to determine how possible it is, because currently the only way that we know how to do this involves allocating a new page every time (to ensure the host isn’t overwriting addresses in use).
  5. Static Allocation: There is a static 4KB array in wasm that holds all output data. The pointer and length are fixed.
  6. Pros
    1. This is pretty simple and clean to use
  7. Cons
    1. Require an extra (2nd) copy step if the developer wants to use data from a previous host memory call (which is likely common)
  8. The host uses a WASM-side defined allocator function in wasm to allocate, and returns a pointer and length tuple (this is what the devnet currently uses).
  9. Pros
    1. This is pretty clean to use
  10. Cons
    1. Increases the size of the WASM program because we use Vector allocation. To get around this we would need our own allocator.
    2. Takes more gas/makes the WASM bytecode long.

We decided on Option 1 for the purpose of simplicity.

Appendix C: FAQ

C.1: How does this list of host functions compare to the Xahau Hooks host functions?

The host functions on this list are heavily inspired by the Hooks host functions. Most of the changes are just naming and simplifying the functions, and reworking how they're organized.

C.2: Can we add a host function for [insert request here]?

Please share any request you have in the comments of this spec.

Some limitations:

  • The transaction engine cannot access historical data - only current ledger state (since nodes aren’t required to hold any amount of past data).
  • Due to security reasons, we don’t want to give host functions write access to raw ledger data (that would make exploits much easier to implement and it would be much harder for us to protect against them).

C.3: Will gas fees be refundable if I pay for too much gas, like EVM?

Not at this time. This may be revisited later, and can be added in a future amendment.

Not all smart contract chains support refundable gas - for example, Solana does not.

C.4: Will transactions that use the WASM VM be testable via simulate?

Yes, though that needs to be tested. This should make it easier for users to estimate gas usage.

C.5: Why not use native WASM floating point?

WebAssembly's native f32 and f64 types are IEEE 754 binary floating-point. While they could be used directly for numeric operations in smart contracts, perhaps with NaN canonicalization to address the one known source of non-determinism in the WASM spec (NaN bit-payload variation when inputs are non-canonical), in practice this would be insufficient for two independent reasons:

First, XRPL uses a custom decimal (base-10) floating-point format, not IEEE 754 binary (base-2). While both formats have a mantissa and exponent, IEEE 754 cannot exactly represent many common decimal values — for example, the decimal value 0.1 becomes a repeating fraction when converted to binary. Any contract that performed decimal arithmetic using native WASM floats could produce results that diverge from rippled, making those contracts incorrect by construction.

Second, and more fundamentally, XRPL's Number arithmetic is itself complex, carefully specified, and subject to change via ledger amendment. The rippled implementation encodes years of decisions about rounding, normalization, overflow handling, and edge cases for decimal calculations. There is no Rust equivalent in this library, and there should not be: porting that logic correctly would be a significant maintenance burden, and any divergence — even a single rounding edge case — would produce a contract that computes results differently from rippled. Worse, if the Number arithmetic is ever changed by a ledger amendment, contracts that embedded their own copy of the logic would silently continue using the old behavior while the rest of the ledger moved to the new one.

The host function design ensures that all contracts always use exactly the arithmetic rippled uses at execution time. No porting, no maintenance, no drift.

C.6: Why not provide a Rust implementation of Number arithmetic in xrpl-wasm-stdlib?

For reasons related to C.5, xrpl-wasm-stdlib deliberately does not ship a Rust implementation of Number arithmetic. Such an implementation would face the same amendment-drift problem defined in C.5: it would be frozen at the version of the logic that existed when it was written. The correct abstraction boundary is the host function interface — contracts call into rippled, rippled's Number class does the math, and the contract receives the result as an opaque 12-byte buffer. This keeps the arithmetic logic in exactly one place. It is also cheaper: a single host call executes the operation in native C++ at a fixed gas cost, whereas the same arithmetic implemented in WASM would be interpreted instruction by instruction and metered accordingly, costing far more gas for the same result.

C.7: Why the 12-byte encoding for XFloat?

Using an unpacked 12-byte layout (8-byte mantissa + 4-byte exponent) rather than existing XRPL serialization formats:

Compared to STAmount (8 bytes): XFloat uses 4 extra bytes, but provides:

  1. Larger mantissa precision: 64-bit signed mantissa vs. 54-bit mantissa in STAmount
  2. Wider exponent range: 32-bit signed exponent vs. 8-bit exponent in STAmount
  3. Simpler layout: Unpacked integer fields are straightforward to serialize and deserialize

The 4 extra bytes per value are negligible given the no_std stack-only model.

Compared to STNumber (14 bytes): XFloat is 2 bytes shorter because it omits the type prefix — the host functions already know they're working with an XFloat, so the prefix is unnecessary.

An alternative considered was adopting STNumber's 14-byte layout directly as the XFloat format. This was rejected in favor of the unpacked 12-byte layout: the 2 extra bytes from the type prefix are wasted since the host functions already know the type from context, and keeping XFloat independent of STNumber's wire format avoids coupling the in-memory buffer convention to a ledger serialization format that could itself change.

C.8: Why are ledger serialization formats unchanged by XFloat?

The 12-byte XFloat format is exclusively a host-function buffer convention. Existing ledger serialization formats — including the 8-byte IouNumber encoding in STAmount — are unchanged by this specification. float_from_iou_value exists to load values from that on-ledger format into XFloat for in-contract computation, without touching how those values are stored or transmitted on the wire. STNumber values reuse the same host function after xrpl-wasm-stdlib decodes the STNumber bytes in Rust (see C.16).

C.9: Why is host function immutability required?

The versioning rules in §5.11 reflect a fundamental constraint of the WASM smart contract platform: deployed contract binaries cannot be updated. A contract compiled against a given set of host function signatures must continue to work correctly on every future version of rippled. This makes host function immutability a hard requirement, not a preference.

Alternative considered — let contracts break: One option is to simply allow host functions to change, and let old contracts stop working. This is simpler for rippled maintainers (no need to maintain old implementations forever) but risky for a financial network: users deploy contracts expecting them to work, and funds could be locked in contracts that suddenly break. This approach was rejected in favor of maintaining backward compatibility, though it could be reconsidered in the future.

Tradeoff: The current design puts the maintenance burden on rippled (keeping deprecated functions callable forever) rather than on contract authors or users. This is a conservative choice appropriate for financial infrastructure.

C.10: Can an XFloat be negative?

Yes. Unlike XRP drop amounts, the XFloat mantissa is a signed 8-byte (i64) integer (§5.8.3), so an XFloat can represent negative values directly — a negative value is encoded with a negative mantissa and the same exponent that would be used for the equivalent positive value.

Note: xrpl-wasm-stdlib will provide an idiomatic Rust XFloat type with normal negative-number semantics (comparisons, sign checks, etc.), so most contract developers won't need to reason about mantissa signs directly.

C.11: How do I negate an XFloat?

There is no dedicated float_negate host function. The simplest approach is to multiply by negative one:

  • Allocate a 12-byte output buffer for negative one, and call float_from_mant_exp with a mantissa of -1 and an exponent of 0 to produce an XFloat equal to -1.
  • Allocate a second 12-byte output buffer for the result, and call float_mul, passing the XFloat you want to negate as the first buffer/length pair and the -1 XFloat from the previous step as the second buffer/length pair.
  • The output buffer now holds the negated XFloat.

Calling float_sub with a zero XFloat as the first operand and the value to negate as the second operand works just as well.

Note: xrpl-wasm-stdlib will provide a helper function (e.g. implementing Rust's Neg trait) that performs this multiply-by-negative-one sequence internally, so most contract developers won't need to call float_from_mant_exp/float_mul directly.

C.12: How do I check if an XFloat is negative?

Use float_cmp against a zero XFloat:

  • Allocate a 12-byte buffer for zero, and call float_from_mant_exp with a mantissa of 0 and an exponent of 0 to produce an XFloat equal to 0.
  • Call float_cmp, passing the XFloat you want to check as the first buffer/length pair and the zero XFloat as the second buffer/length pair.
  • A negative return value means the XFloat is negative; zero means it's exactly zero; a positive return value means it's positive.

Note: xrpl-wasm-stdlib will provide a helper function (e.g. is_negative()) that performs this zero-comparison internally, so most contract developers won't need to construct a zero XFloat or call float_cmp directly.

C.13: How do I get an XFloat from an STAmount?

The approach depends on the STAmount's underlying type:

IOU amounts: Use float_from_iou_value (see §5.8). This function does not take the whole 48-byte STAmount (amount + currency + issuer) — it takes only the 8-byte IOU amount field. Parse the STAmount down to its IOUNumber amount bytes first, then pass those 8 bytes in:

  • Extract the 8-byte IOU amount field from the parsed STAmount.
  • Allocate a 12-byte output buffer, and call float_from_iou_value, passing the 8-byte IOU amount field as the input buffer/length pair and the output buffer/length pair to receive the result.
  • The output buffer now holds the XFloat representation of that amount.

float_from_stamount was the earlier name for this function; it has been renamed to float_from_iou_value to make clear that it operates on the 8-byte IOU amount value, not the whole STAmount structure.

XRP and MPT amounts: These are plain integers rather than the packed IOU encoding, so use float_from_mant_exp instead — pass the amount as the mantissa and 0 as the exponent.

Note: xrpl-wasm-stdlib will provide a helper function that performs the appropriate extraction and conversion internally for each STAmount type, so most contract developers won't need to parse STAmount bytes or choose the right host function directly.

C.14: How do I create an STAmount or STNumber from an XFloat?

There is currently no host function to go the other direction for either format — no float_to_iou_number_amount and no float_to_stnumber. This is a known gap: contracts today don't need to emit transactions or ledger data requiring the raw STAmount/IOUNumber or STNumber byte format, since XFloat is only used for in-contract computation (§5.8.1). As a result, a value that started as an STAmount, was converted to an XFloat via float_from_iou_value, and then had arithmetic applied to it (see C.15) has no way to be converted back into the 8-byte value the ledger expects; the same is true for STNumber, which is decoded via xrpl-wasm-stdlib and float_from_iou_value (see C.16).

This gap is expected to be addressed if transaction emissions ever becomes a supported feature in the WASM layer — at which point float_to_iou_number_amount and/or float_to_stnumber could be added as new host functions.

Note: If/when reverse conversion host functions are added, xrpl-wasm-stdlib is expected to expose them via idiomatic Rust helpers as well, consistent with the rest of the float API — so contract developers still won't need to construct STAmount/IOUNumber/STNumber bytes by hand.

C.15: How do I add two STAmounts together?

Convert each STAmount's 8-byte IOU amount field to an XFloat via float_from_iou_value, then use float_add:

  • Extract the 8-byte IOU amount field from each of the two STAmounts.
  • Allocate a 12-byte output buffer for each, and call float_from_iou_value once per amount to produce two XFloats.
  • Allocate a third 12-byte output buffer, and call float_add, passing the two XFloats as the first and second buffer/length pairs and the third buffer/length pair to receive the sum.
  • The output buffer now holds the sum as an XFloat.

As noted in C.14, the sum exists only as an XFloat — there is currently no host function to convert it back into STAmount-compatible bytes.

C.16: How do I get an XFloat from an STNumber?

STNumber is decoded in Rust by xrpl-wasm-stdlib, not by a dedicated host function. For the IOU-precision case, the stdlib types the decoded 8-byte value as NarrowNumber — a Rust type that is layout-identical to the IouNumber type used in STAmount's IOU amount field. Once decoded to a NarrowNumber, use float_from_iou_value (see §5.8) exactly as you would for an STAmount's IOU amount field:

  • Decode the STNumber bytes to a NarrowNumber using xrpl-wasm-stdlib.
  • Allocate a 12-byte output buffer, and call float_from_iou_value, passing the NarrowNumber's 8 bytes as the input buffer/length pair and the output buffer/length pair to receive the result.
  • The output buffer now holds the XFloat representation of that value.

There is no dedicated float_from_stnumber host function — decoding happens entirely in Rust, and only the resulting NarrowNumber bytes cross into a host function call.

Note: xrpl-wasm-stdlib will provide helper functions that perform these conversions and the addition internally, so most contract developers will use an idiomatic Rust API (e.g. adding two amount types directly) rather than manipulating buffers and pointers themselves.