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¶
Using Smart Escrows as an example:
- 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
Bytecodeto determine if the escrow is finishable). - Enter the WASM engine, where the WASM environment is set up to run the code.
- Run the WASM code, using host functions to fetch on-ledger information.
- 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
rippledand 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
EscrowFinishtransaction (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
datafield 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 valuemantissa × 10^exponent. The mantissa written here is the value returned by xrpld'sNumber::mantissa()accessor, i.e. the external view of the number, not the internal representation. Internally, xrpld'sNumbernormalizes its unsigned mantissa to 10^18 ≤ |m| < 10^19 in the default large-scale mode (active when theSingleAssetVaultorLendingProtocolamendment is enabled), or to 10^15 ≤ |m| < 10^16 in the legacy small-scale mode. When the internal mantissa exceedsi64::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 are0x00. - 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:
- New host functions MAY be added at any time without breaking existing contracts. Contracts that do not call a new function are unaffected.
- 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. - 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¶
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¶
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:
- Caller-Allocated: The contract developer (on the WASM side) allocates fixed size arrays for returning data. The user knows the pointer and the length.
- This is really easy to implement, but means that the WASM dev needs to do their own memory allocation.
- Host-Allocated: The host (rippled) allocates WASM memory in host functions and passes the pointer and the length to the WASM program.
- 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).
- Static Allocation: There is a static 4KB array in wasm that holds all output data. The pointer and length are fixed.
- Pros
- This is pretty simple and clean to use
- Cons
- Require an extra (2nd) copy step if the developer wants to use data from a previous host memory call (which is likely common)
- 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).
- Pros
- This is pretty clean to use
- Cons
- Increases the size of the WASM program because we use Vector allocation. To get around this we would need our own allocator.
- 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:
- Larger mantissa precision: 64-bit signed mantissa vs. 54-bit mantissa in
STAmount - Wider exponent range: 32-bit signed exponent vs. 8-bit exponent in
STAmount - 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_expwith a mantissa of-1and an exponent of0to produce anXFloatequal to-1. - Allocate a second 12-byte output buffer for the result, and call
float_mul, passing theXFloatyou want to negate as the first buffer/length pair and the-1XFloatfrom 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_expwith a mantissa of0and an exponent of0to produce anXFloatequal to0. - Call
float_cmp, passing theXFloatyou want to check as the first buffer/length pair and the zeroXFloatas the second buffer/length pair. - A negative return value means the
XFloatis 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
XFloatrepresentation 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_valueonce per amount to produce twoXFloats. - Allocate a third 12-byte output buffer, and call
float_add, passing the twoXFloats 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
STNumberbytes to aNarrowNumberusingxrpl-wasm-stdlib. - Allocate a 12-byte output buffer, and call
float_from_iou_value, passing theNarrowNumber'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
XFloatrepresentation 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.